-
Notifications
You must be signed in to change notification settings - Fork 314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Email spam security #641
base: main
Are you sure you want to change the base?
Email spam security #641
Conversation
- Updated the `getValidationRules` method in various integration handlers (Discord, Email, Google Sheets, Slack, Webhook, Zapier) to accept an optional `Form` parameter, allowing for context-aware validation. - Enhanced the `EmailIntegration` handler to enforce restrictions based on user plans, ensuring free users can only create one email integration per form and can only send to a single email address. - Added a new test suite for `EmailIntegration` to validate the new restrictions and ensure proper functionality for both free and pro users. - Introduced loading state management in the `IntegrationModal` component to improve user experience during save operations. These changes improve the flexibility and user experience of form integrations, particularly for email handling.
WalkthroughThe changes in this pull request involve enhancements to the user registration process and integration handling within the application. Key updates include the addition of rate limiting middleware in the Changes
Possibly related PRs
Suggested reviewers
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
🧹 Outside diff range and nitpick comments (16)
api/app/Integrations/Handlers/WebhookIntegration.php (2)
9-9
: Consider utilizing the Form parameter for context-specific validationThe Form parameter is currently unused. Consider leveraging it to implement form-specific validation rules, such as:
- Different URL patterns for different form types
- Specific domain restrictions based on form context
- Custom validation rules based on form settings
Example implementation:
public static function getValidationRules(?Form $form): array { + $baseRules = [ + 'webhook_url' => [ + 'required', + 'url', + ] + ]; + + if ($form && $form->requires_strict_validation) { + $baseRules['webhook_url'][] = 'regex:/^https:\/\/api\.(trusted-domain)\.(com|org)\/webhook/'; + } + + return $baseRules; }
Consider implementing rate limiting for webhook endpoints
Based on the codebase analysis, while there's basic URL validation, the webhook endpoints could benefit from additional security measures. I notice that:
- The application already implements rate limiting in various places (e.g., API, authentication) but not specifically for webhook endpoints
- There's no existing domain allowlist/blocklist configuration for webhooks
- The webhook integration is part of a larger forms system with existing security considerations
Recommended implementation:
public static function getValidationRules(?Form $form): array { return [ 'webhook_url' => [ 'required', 'url', 'not_regex:/^https?:\/\/(\d{1,3}\.){3}\d{1,3}/', // Prevent IP-based URLs function ($attribute, $value, $fail) { $domain = parse_url($value, PHP_URL_HOST); if (filter_var($domain, FILTER_VALIDATE_IP)) { $fail('IP-based webhook URLs are not allowed.'); } } ] ]; }Additionally:
- Add the
throttle:10,1
middleware to webhook routes to prevent abuse- Consider implementing a webhook signing mechanism for secure payload delivery
🔗 Analysis chain
Line range hint
9-14
: Consider enhancing webhook URL validation for better securityWhile the basic URL validation is present, given this PR's focus on spam security, consider implementing additional security measures:
- Domain allowlist/blocklist validation
- Rate limiting for webhook endpoints
- Webhook URL pattern restrictions
Here's a suggested implementation with enhanced security:
public static function getValidationRules(?Form $form): array { return [ - 'webhook_url' => 'required|url' + 'webhook_url' => [ + 'required', + 'url', + 'not_regex:/^https?:\/\/(\d{1,3}\.){3}\d{1,3}/', // Prevent IP-based URLs + function ($attribute, $value, $fail) { + $blockedDomains = config('security.blocked_domains', []); + $domain = parse_url($value, PHP_URL_HOST); + if (in_array($domain, $blockedDomains)) { + $fail('This webhook domain is not allowed.'); + } + } + ] ]; }Let's verify if there are any existing security measures or configurations for webhook URLs:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Check for existing webhook security configurations rg -l "webhook.*security|blocked.*domains|allowed.*domains" config/ # Check for any rate limiting on webhook endpoints rg -l "throttle.*webhook|rate.*limit.*webhook" app/Http/Middleware/Length of output: 248
Script:
#!/bin/bash # Let's first locate the config and middleware directories fd -t d "config|middleware" -i # Check for any existing webhook-related configurations rg -i "webhook" --type php # Check for any rate limiting or throttling implementations rg -i "throttle|rate.?limit" --type php # Look for any security-related configurations or middleware rg -i "security|blocked.?domains|allowed.?domains" --type phpLength of output: 16425
api/tests/Feature/UserManagementTest.php (1)
68-68
: Optimize HCaptcha token usage in error scenariosThe tests should verify that HCaptcha validation is not performed when the registration fails due to other validations (expired/invalid invite token). This prevents unnecessary API calls and potential costs in production.
Consider validating the invite token before performing HCaptcha verification:
+it('skips hCaptcha validation for expired invite token', function () { + // Setup expired token... + $response = $this->postJson('/register', [...]); + $response->assertStatus(400); + Http::assertNothingSent(); +});Also applies to: 98-98, 115-115
api/app/Http/Requests/Integration/FormIntegrationsRequest.php (1)
22-22
: Use$request->route('id')
instead of globalrequest()
helperFor consistency and better testability, use the
$request
object provided in the constructor rather than the globalrequest()
helper.Apply this diff to make the change:
- $this->form = Form::findOrFail(request()->route('id')); + $this->form = Form::findOrFail($request->route('id'));api/app/Integrations/Handlers/EmailIntegration.php (2)
Line range hint
18-54
: Potential null reference on$form
ingetValidationRules
The
$form
parameter is nullable, but the method uses$form
without checking if it is null. This could lead to a fatal error if$form
isnull
. Please ensure$form
is not null before accessing its properties.Consider enforcing a non-null
$form
parameter:- public static function getValidationRules(?Form $form): array + public static function getValidationRules(Form $form): arrayAlternatively, add a null check at the beginning of the method:
if (is_null($form)) { throw new \InvalidArgumentException('Form cannot be null.'); }
36-40
: Enhance email address parsing in closure validationThe current logic may not accurately parse email addresses containing commas or newlines. Consider using a more reliable method to split and validate multiple email addresses.
Example using regular expressions:
$emails = preg_split('/[\s,]+/', trim($value)); if (count($emails) > 1) { $fail('You can only send to a single email address on the free plan. Please upgrade to the Pro plan to create a new integration.'); }api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php (1)
14-14
: Consider indexing if queryingmeta
fieldIf you plan to query the
meta
JSON column frequently, consider adding appropriate indexes to optimize performance.Example:
$table->json('meta')->default('{}')->index();Be aware that indexing JSON fields may have limitations depending on the database.
api/app/Integrations/Handlers/ZapierIntegration.php (1)
20-20
: Consider removing nullable type if$form
is requiredIf all integration handlers require a
Form
instance for validation, consider removing the nullable type to enforce this requirement.- public static function getValidationRules(?Form $form): array + public static function getValidationRules(Form $form): arrayapi/tests/Feature/RegisterTest.php (1)
9-11
: Consider using dynamic mock tokens for hCaptcha testsWhile mocking hCaptcha responses is correct, using a hardcoded 'test-token' might make tests brittle. Consider generating dynamic tokens or extracting to a constant.
+ const MOCK_HCAPTCHA_TOKEN = 'test-token'; Http::fake([ ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true]) ]); // ... - 'h-captcha-response' => 'test-token', // Mock token for testing + 'h-captcha-response' => MOCK_HCAPTCHA_TOKEN,Also applies to: 20-20
client/components/open/integrations/components/IntegrationModal.vue (1)
Line range hint
115-119
: Enhance error handling specificityThe catch block could be more specific about different types of errors to help identify potential security issues.
- try { - alert.error(error.data.message) - } catch (e) { - alert.error("An error occurred while saving the integration") - } + if (error.response?.status === 429) { + alert.error("Too many attempts. Please try again later.") + } else if (error.response?.data?.message) { + alert.error(error.response.data.message) + } else { + alert.error("An error occurred while saving the integration") + }api/app/Integrations/Handlers/AbstractIntegrationHandler.php (1)
97-97
: Consider adding base spam prevention validation rulesSince this PR focuses on email spam security, consider adding some base validation rules in the abstract class that all integrations must include. For example:
- Rate limiting validation
- Spam keyword detection
- Input length restrictions
- Character set validation
Would you like me to provide an example implementation of these security measures?
api/app/Integrations/Handlers/DiscordIntegration.php (2)
Line range hint
13-22
: Add spam prevention validation rulesWhile restricting to pro users helps, additional validation rules should be added to prevent spam:
- Rate limiting validation
- Input sanitization for webhook messages
- Maximum length restrictions for submission data
Add these validation rules:
public static function getValidationRules(?Form $form): array { return [ 'discord_webhook_url' => 'required|url|starts_with:https://discord.com/api/webhooks', + 'message' => ['required', 'string', 'max:2000', 'not_regex:/(?:http|https|www\\.)[^\\s]+/i'], 'include_submission_data' => 'boolean', 'link_open_form' => 'boolean', 'link_edit_form' => 'boolean', 'views_submissions_count' => 'boolean', 'link_edit_submission' => 'boolean' ]; }
Line range hint
31-33
: Add rate limiting to shouldRun methodThe shouldRun method should include rate limiting checks to prevent spam attacks.
Consider implementing rate limiting:
protected function shouldRun(): bool { - return !is_null($this->getWebhookUrl()) && $this->form->is_pro && parent::shouldRun(); + return !is_null($this->getWebhookUrl()) + && $this->form->is_pro + && !$this->isRateLimited() + && parent::shouldRun(); } + +private function isRateLimited(): bool +{ + $key = "discord_integration:{$this->form->id}"; + return !RateLimiter::attempt($key, 60, function() {}, 60); +}api/app/Integrations/Handlers/SlackIntegration.php (1)
Line range hint
13-22
: Add spam prevention validation rules and consider code reuseThe validation rules are similar to DiscordIntegration. Consider:
- Creating a shared trait for webhook security
- Adding spam prevention rules
- Implementing rate limiting
Create a new trait for shared security features:
trait WebhookSecurityTrait { protected function getBaseValidationRules(): array { return [ 'message' => ['required', 'string', 'max:2000', 'not_regex:/(?:http|https|www\\.)[^\\s]+/i'], 'include_submission_data' => 'boolean', 'link_open_form' => 'boolean', 'link_edit_form' => 'boolean', 'views_submissions_count' => 'boolean', 'link_edit_submission' => 'boolean' ]; } protected function isRateLimited(): bool { $key = "webhook_integration:{$this->form->id}"; return !RateLimiter::attempt($key, 60, function() {}, 60); } }Then update the validation rules:
public static function getValidationRules(?Form $form): array { - return [ + return array_merge(self::getBaseValidationRules(), [ 'slack_webhook_url' => 'required|url|starts_with:https://hooks.slack.com/', - 'include_submission_data' => 'boolean', - 'link_open_form' => 'boolean', - 'link_edit_form' => 'boolean', - 'views_submissions_count' => 'boolean', - 'link_edit_submission' => 'boolean' - ]; + ]); }client/components/pages/auth/components/RegisterForm.vue (2)
182-184
: Consider using camelCase for form field names.The form field
h-captcha-response
uses kebab-case, which is unusual in JavaScript. Consider using camelCase (hCaptchaResponse
) for consistency, unless this specific naming is required by the hCaptcha API.
Line range hint
1-324
: Consider implementing a dedicated hCaptcha wrapper component.The current implementation directly integrates hCaptcha into the registration form. Consider creating a reusable wrapper component that:
- Encapsulates the hCaptcha logic
- Provides a cleaner interface for getting/setting the response
- Handles self-hosted conditions internally
- Can be reused in other forms that need spam protection
This would improve maintainability and make it easier to add spam protection to other forms in the future.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (16)
api/app/Http/Controllers/Auth/RegisterController.php
(4 hunks)api/app/Http/Requests/Integration/FormIntegrationsRequest.php
(3 hunks)api/app/Integrations/Handlers/AbstractIntegrationHandler.php
(1 hunks)api/app/Integrations/Handlers/DiscordIntegration.php
(1 hunks)api/app/Integrations/Handlers/EmailIntegration.php
(2 hunks)api/app/Integrations/Handlers/GoogleSheetsIntegration.php
(2 hunks)api/app/Integrations/Handlers/SlackIntegration.php
(1 hunks)api/app/Integrations/Handlers/WebhookIntegration.php
(1 hunks)api/app/Integrations/Handlers/ZapierIntegration.php
(2 hunks)api/app/Models/User.php
(3 hunks)api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php
(1 hunks)api/tests/Feature/Integrations/Email/EmailIntegrationTest.php
(1 hunks)api/tests/Feature/RegisterTest.php
(3 hunks)api/tests/Feature/UserManagementTest.php
(6 hunks)client/components/open/integrations/components/IntegrationModal.vue
(4 hunks)client/components/pages/auth/components/RegisterForm.vue
(6 hunks)
🔇 Additional comments (23)
api/app/Integrations/Handlers/WebhookIntegration.php (1)
5-5
: LGTM!
The Form import is correctly added to support the new parameter type.
api/tests/Feature/UserManagementTest.php (1)
Line range hint 1-156
: Security Review: Additional Test Coverage Needed
While the addition of HCaptcha is a good step towards preventing spam registrations, the test coverage should be expanded to include:
- Edge cases in HCaptcha validation
- Integration with other security measures (rate limiting, email verification)
- Proper error handling and token optimization
- Protection against automated registration attempts
These improvements will ensure the spam protection measures are robust and effective.
Let's verify the current security measures:
api/app/Http/Requests/Integration/FormIntegrationsRequest.php (2)
5-5
: Importing Form
model
The Form
model is correctly imported to access form data within the request.
83-85
: Passing $this->form
enhances context-aware validation
By passing the form instance to getValidationRules
, validation can be tailored based on the specific form, improving flexibility and accuracy.
api/app/Integrations/Handlers/EmailIntegration.php (4)
5-6
: Importing Form
and FormIntegration
models
The inclusion of the Form
and FormIntegration
models is necessary for accessing form data and integration capabilities.
12-12
: Importing ValidationException
for proper exception handling
The addition of ValidationException
allows for more precise exception handling and user feedback during validation.
43-43
: Verify the correctness of the route parameter 'integrationid'
Ensure that 'integrationid'
is the correct route parameter. If it should be 'integration_id'
, this condition may not work as intended.
Check your route definitions to confirm the parameter name.
47-51
: Appropriate use of ValidationException
Throwing ValidationException
provides clear feedback to the user when validation constraints are not met.
api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php (1)
14-14
: Verify JSON column default value compatibility
Setting a default value for a JSON column may not be supported in some database versions (e.g., older MySQL versions). Ensure your database supports default JSON values.
Consider checking the minimum database version requirements and testing the migration on the target databases.
api/app/Integrations/Handlers/ZapierIntegration.php (1)
20-20
: Ensure consistency in method signatures across integration handlers
The getValidationRules
method now accepts a ?Form $form
parameter. Even if it's not used in this class, this change maintains consistency with other integration handlers.
api/tests/Feature/RegisterTest.php (2)
25-28
: LGTM: Good security practice tracking registration IPs
Properly testing the storage of registration IP addresses is crucial for security auditing and abuse prevention.
75-87
: LGTM: Comprehensive hCaptcha validation for production
Good security practice ensuring hCaptcha is required in production environment.
client/components/open/integrations/components/IntegrationModal.vue (1)
103-104
: LGTM: Good protection against duplicate submissions
The loading state check helps prevent duplicate form submissions, which is good for preventing spam and ensuring data integrity.
api/app/Integrations/Handlers/GoogleSheetsIntegration.php (2)
Line range hint 44-49
: Review logging of sensitive information
While logging form identifiers is useful for debugging, ensure these logs are properly secured and not exposed to unauthorized users.
✅ Verification successful
Let me gather more context about the logging patterns to better understand how form IDs and slugs are handled in logs.
Let me try a different approach to check the logging patterns.
Form identifiers in logs are appropriately used and secured
The logging of form IDs and slugs appears to be implemented securely and appropriately:
- Form IDs are only logged in debug/error contexts for operational tracking
- Logs are used in controlled environments (integration handlers)
- Sensitive data like email recipients and workspace details are logged separately
- Error logging includes necessary context for troubleshooting integration issues
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check for other instances of form ID/slug logging
rg "form_(id|slug)" --type php
Length of output: 5991
Script:
#!/bin/bash
# Check for logging patterns around form data
rg "Log::(debug|info|warning|error).+form" --type php -A 2
Length of output: 60
Script:
#!/bin/bash
# Check for logging patterns in the integration handlers
rg "Log::" api/app/Integrations/Handlers/ -A 2
Length of output: 1365
26-28
: Consider adding validation rules for spam prevention
The empty validation rules might need to be enhanced with rate limiting or other spam prevention measures, especially since this PR focuses on email security.
api/app/Integrations/Handlers/SlackIntegration.php (1)
Line range hint 31-33
: Add rate limiting to shouldRun method
Similar to DiscordIntegration, implement rate limiting to prevent spam.
The same rate limiting implementation suggested for DiscordIntegration should be applied here.
api/app/Http/Controllers/Auth/RegisterController.php (1)
73-75
: Verify security implications of skipping hCaptcha in self-hosted mode
While the hCaptcha integration helps prevent automated registrations, skipping this validation in self-hosted mode could create a security vulnerability if the instance is exposed to the internet.
Consider:
- Making hCaptcha configurable in self-hosted mode
- Implementing alternative spam prevention for self-hosted instances
✅ Verification successful
Rate limiting provides adequate security for self-hosted registrations
The registration endpoint is already protected by multiple layers of rate limiting:
- 5 attempts per minute
- 30 attempts per hour
- Global API rate limit of 100 requests per minute
These rate limiting measures provide robust protection against automated registrations and brute force attacks, even without hCaptcha in self-hosted mode. The security concern is adequately addressed through these existing controls.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check if there are any existing security measures for self-hosted mode
rg -A 5 "self_hosted.*registration"
Length of output: 37
Script:
#!/bin/bash
# Let's check for any registration-related security measures in the codebase
rg -A 5 "registration|register" config/
Length of output: 89
Script:
#!/bin/bash
# Check for other validation rules in the RegisterController
ast-grep --pattern 'function validator($$$) { $$$ }' api/app/Http/Controllers/Auth/RegisterController.php
Length of output: 107
Script:
#!/bin/bash
# Look for any rate limiting or throttling mechanisms
rg -A 5 "throttle|RateLimit" api/app/Http/
Length of output: 2642
Script:
#!/bin/bash
# Check if there are any other security middleware applied to registration
rg -A 5 "middleware.*register" api/routes/
Length of output: 44
client/components/pages/auth/components/RegisterForm.vue (6)
148-148
: LGTM!
The VueHcaptcha component is properly registered.
213-216
: LGTM!
The hCaptcha reference is properly initialized based on the self-hosted condition.
55-69
: LGTM! Verify hCaptcha integration in self-hosted environments.
The hCaptcha integration is well-implemented with proper error handling and conditional rendering.
Let's verify the self-hosted configuration handling:
✅ Verification successful
hCaptcha integration correctly handles self-hosted environments
The verification confirms that the self_hosted
feature flag is properly integrated across the application:
- The RegisterForm correctly uses
isSelfHosted
computed property derived fromuseFeatureFlag('self_hosted')
- The conditional rendering
v-if="!isSelfHosted"
ensures hCaptcha is only shown in cloud environments - This aligns with the broader application's self-hosted vs cloud deployment strategy, as evidenced by similar patterns in other components
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for self-hosted feature flag configuration
rg -A 3 "self_hosted" --type js
Length of output: 3011
179-180
: Verify the necessity of utm_data addition.
The addition of utm_data
seems unrelated to the hCaptcha integration. Please verify if this change belongs in this PR.
✅ Verification successful
The utm_data addition is a valid part of user registration tracking
The codebase analysis shows that utm_data
is a well-integrated feature for tracking user registration sources:
- There's a dedicated migration
2024_09_18_091846_add_utm_data_to_users_table.php
adding this field - The User model properly declares it in fillable and casts arrays
- It's consistently used in both regular registration (RegisterController) and OAuth registration flows
- The implementation in RegisterForm.vue matches the backend expectations
The change belongs in this PR as it's part of the registration form data structure, even though it's separate from the hCaptcha integration.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check usage of utm_data in the codebase
rg -A 3 "utm_data"
Length of output: 3734
188-190
: LGTM! Verify secure handling of the site key.
The computed property correctly retrieves the hCaptcha site key. Ensure the key is properly secured in the runtime configuration.
✅ Verification successful
LGTM! The hCaptcha site key is properly handled.
The implementation is secure as:
- The site key is properly sourced from environment variables via
NUXT_PUBLIC_H_CAPTCHA_SITE_KEY
- It's safely exposed through the runtime config system in
client/runtimeConfig.js
- The key is only used with the official
@hcaptcha/vue3-hcaptcha
component - There are no hardcoded keys in the codebase
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check for potential hardcoded site keys
rg -A 3 "hCaptchaSiteKey.*=.*['\"]" --type js
Length of output: 49
Script:
#!/bin/bash
# Search for hCaptcha related configuration in nuxt config files
fd -e js -e ts "nuxt.config" --exec rg -l "hCaptcha"
# Search for runtime config setup
rg -i "runtimeconfig.*hcaptcha" --type=vue --type=js --type=ts -A 3
Length of output: 148
Script:
#!/bin/bash
# Search for hCaptcha configuration in config files
rg -i "hcaptcha" --type-add 'vue:*.vue' --type=vue --type=js --type=ts -A 3
# Look for environment variable usage related to hCaptcha
rg -i "HCAPTCHA" --type=env --type=js --type=ts -A 2
Length of output: 5148
143-144
: Verify the necessity of the workspaces import.
The fetchAllWorkspaces
import appears unrelated to the hCaptcha integration. Please verify if this import is necessary for this component.
✅ Verification successful
The fetchAllWorkspaces
import is necessary and correctly used
The import is used in the register
method after successful user registration to fetch and set workspace data in the store. This is part of the post-registration flow where the application initializes the user's workspace data.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check usage of fetchAllWorkspaces in the component
ast-grep --pattern 'fetchAllWorkspaces($$$)'
Length of output: 119
Script:
#!/bin/bash
# Check if fetchAllWorkspaces is used in RegisterForm.vue
rg "fetchAllWorkspaces" "client/components/pages/auth/components/RegisterForm.vue"
# Get the content of RegisterForm.vue to understand the component's logic
cat "client/components/pages/auth/components/RegisterForm.vue"
Length of output: 8075
@@ -138,6 +147,7 @@ | |||
'password_confirmation' => 'secret', | |||
'agree_terms' => true, | |||
'invite_token' => $token, | |||
'h-captcha-response' => 'test-token', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comprehensive spam protection test cases
The current test suite focuses on invite token validation but lacks coverage for important spam prevention scenarios:
- Rate limiting of registration attempts
- Multiple failed HCaptcha attempts
- Email verification workflow
- IP-based restrictions
Would you like me to help create additional test cases for these scenarios? This would help ensure the email spam security measures are working as intended.
@@ -31,6 +36,7 @@ | |||
'password_confirmation' => 'secret', | |||
'agree_terms' => true, | |||
'invite_token' => $token, | |||
'h-captcha-response' => 'test-token', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enhance HCaptcha validation test coverage
The test uses a static 'test-token' value without verifying if the HCaptcha validation was actually performed. Consider:
- Asserting that the HCaptcha verification endpoint was called
- Verifying the token value was passed correctly
Example enhancement:
+ Http::fake([
+ ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true])
+ ]);
$response = $this->postJson('/register', [
// ... other fields ...
'h-captcha-response' => 'test-token',
]);
$response->assertSuccessful();
+ Http::assertSent(function ($request) {
+ return $request->url() === ValidHCaptcha::H_CAPTCHA_VERIFY_URL
+ && $request->data()['response'] === 'test-token';
+ });
Committable suggestion skipped: line range outside the PR's diff.
Http::fake([ | ||
ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true]) | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add test cases for HCaptcha verification failures
The current setup always mocks HCaptcha to return success. To ensure robust spam protection, we should also test:
- Invalid/expired tokens
- Rate-limited responses
- Network failures
Example implementation:
beforeEach(function () {
$this->user = $this->actingAsProUser();
$this->workspace = $this->createUserWorkspace($this->user);
- Http::fake([
- ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true])
- ]);
});
+it('prevents registration with invalid hCaptcha token', function () {
+ Http::fake([
+ ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => false])
+ ]);
+ // Add test implementation
+});
Committable suggestion skipped: line range outside the PR's diff.
@@ -14,9 +15,11 @@ class FormIntegrationsRequest extends FormRequest | |||
public array $integrationRules = []; | |||
|
|||
private ?string $integrationClassName = null; | |||
private ?Form $form = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure $integrationClassName
is initialized to prevent null reference errors
Since methods like attributes()
and isOAuthRequired()
rely on $this->integrationClassName
, ensure it is initialized even when $request->integration_id
is not provided to prevent potential null reference errors.
Consider initializing $integrationClassName
or adding checks before its usage.
$this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute | ||
$this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider implementing stricter rate limits for registration
While multiple rate limits are good, the current limits (5/min, 30/hour) might be too permissive for a registration endpoint. Consider reducing these limits to prevent potential abuse:
- 3 attempts per hour is typically sufficient for legitimate users
- Current limits could allow automated spam account creation
- $this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute
- $this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour
+ $this->middleware('throttle:3,60')->only('register'); // 3 attempts per hour
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
$this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute | |
$this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour | |
$this->middleware('throttle:3,60')->only('register'); // 3 attempts per hour |
@@ -84,6 +93,7 @@ | |||
'password' => bcrypt($data['password']), | |||
'hear_about_us' => $data['hear_about_us'], | |||
'utm_data' => array_key_exists('utm_data', $data) ? $data['utm_data'] : null, | |||
'meta' => ['registration_ip' => request()->ip()], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review IP address storage for privacy compliance
Storing IP addresses has privacy implications and might be subject to data protection regulations (GDPR, CCPA):
- Consider hashing the IP address
- Implement a retention policy
- Document the purpose of collection
- 'meta' => ['registration_ip' => request()->ip()],
+ 'meta' => ['registration_ip' => hash('sha256', request()->ip())],
Committable suggestion skipped: line range outside the PR's diff.
@@ -33,6 +33,7 @@ class User extends Authenticatable implements JWTSubject | |||
'password', | |||
'hear_about_us', | |||
'utm_data', | |||
'meta' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enhance security for the meta attribute
While it's good that the meta field is hidden, being fillable without validation poses risks:
- Mass assignment could allow injection of unexpected data
- No schema validation for meta contents
Consider:
- Remove from fillable and use explicit setters
- Add validation for meta contents
- Implement strict typing or schema validation
- protected $fillable = [
- 'name',
- 'email',
- 'password',
- 'hear_about_us',
- 'utm_data',
- 'meta'
- ];
+ protected $fillable = [
+ 'name',
+ 'email',
+ 'password',
+ 'hear_about_us',
+ 'utm_data',
+ ];
+ public function setMeta(array $meta): void
+ {
+ $allowedKeys = ['registration_ip'];
+ $filteredMeta = array_intersect_key($meta, array_flip($allowedKeys));
+ $this->attributes['meta'] = json_encode($filteredMeta);
+ }
Also applies to: 48-48, 61-61
test('free user can update their single email integration', function () { | ||
$user = $this->actingAsUser(); | ||
$workspace = $this->createUserWorkspace($user); | ||
$form = $this->createForm($user, $workspace); | ||
|
||
// Create initial integration | ||
$response = $this->postJson(route('open.forms.integration.create', $form), [ | ||
'integration_id' => 'email', | ||
'status' => true, | ||
'settings' => [ | ||
'send_to' => '[email protected]', | ||
'sender_name' => 'Test Sender', | ||
'subject' => 'Test Subject', | ||
'email_content' => 'Test Content', | ||
'include_submission_data' => true | ||
] | ||
]); | ||
|
||
$response->assertSuccessful(); | ||
$integrationId = $response->json('form_integration.id'); | ||
|
||
// Update the integration | ||
$response = $this->putJson(route('open.forms.integration.update', [$form, $integrationId]), [ | ||
'integration_id' => 'email', | ||
'status' => true, | ||
'settings' => [ | ||
'send_to' => '[email protected]', | ||
'sender_name' => 'Updated Sender', | ||
'subject' => 'Updated Subject', | ||
'email_content' => 'Updated Content', | ||
'include_submission_data' => true | ||
] | ||
]); | ||
|
||
$response->assertSuccessful(); | ||
|
||
$integration = FormIntegration::find($integrationId); | ||
expect($integration->data->send_to)->toBe('[email protected]'); | ||
expect($integration->data->sender_name)->toBe('Updated Sender'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add content validation test cases
The current tests should be extended to cover content security scenarios:
- HTML/Script injection in email content
- Maximum content size limits
- Content sanitization
Add these test cases:
+test('it sanitizes html in email content', function () {
+ $user = $this->actingAsUser();
+ $workspace = $this->createUserWorkspace($user);
+ $form = $this->createForm($user, $workspace);
+
+ $response = $this->postJson(route('open.forms.integration.create', $form), [
+ 'integration_id' => 'email',
+ 'status' => true,
+ 'settings' => [
+ 'send_to' => '[email protected]',
+ 'sender_name' => 'Test Sender',
+ 'subject' => 'Test Subject',
+ 'email_content' => '<script>alert("xss")</script>Test Content',
+ 'include_submission_data' => true
+ ]
+ ]);
+
+ $response->assertStatus(422);
+});
Committable suggestion skipped: line range outside the PR's diff.
test('free user cannot add multiple emails', function () { | ||
$user = $this->actingAsUser(); | ||
$workspace = $this->createUserWorkspace($user); | ||
$form = $this->createForm($user, $workspace); | ||
|
||
$response = $this->postJson(route('open.forms.integration.create', $form), [ | ||
'integration_id' => 'email', | ||
'status' => true, | ||
'settings' => [ | ||
'send_to' => "[email protected]\[email protected]", | ||
'sender_name' => 'Test Sender', | ||
'subject' => 'Test Subject', | ||
'email_content' => 'Test Content', | ||
'include_submission_data' => true | ||
] | ||
]); | ||
|
||
$response->assertStatus(422) | ||
->assertJsonValidationErrors(['settings.send_to']) | ||
->assertJson([ | ||
'errors' => [ | ||
'settings.send_to' => ['You can only send to a single email address on the free plan. Please upgrade to the Pro plan to create a new integration.'] | ||
] | ||
]); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add security-focused test cases for email validation
While the current tests cover business rules well, consider adding security-focused test cases:
- Email header injection prevention
- Email address format validation
- Large payload handling
Add these test cases:
+test('it prevents email header injection', function () {
+ $user = $this->actingAsUser();
+ $workspace = $this->createUserWorkspace($user);
+ $form = $this->createForm($user, $workspace);
+
+ $response = $this->postJson(route('open.forms.integration.create', $form), [
+ 'integration_id' => 'email',
+ 'status' => true,
+ 'settings' => [
+ 'send_to' => "[email protected]\nBcc: [email protected]",
+ 'sender_name' => "Innocent Name\nBcc: [email protected]",
+ 'subject' => "Test\nBcc: [email protected]",
+ 'email_content' => 'Test Content',
+ 'include_submission_data' => true
+ ]
+ ]);
+
+ $response->assertStatus(422);
+});
Committable suggestion skipped: line range outside the PR's diff.
if (!this.isSelfHosted) { | ||
this.form['h-captcha-response'] = document.getElementsByName('h-captcha-response')[0].value | ||
this.hcaptcha.reset() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid direct DOM access for hCaptcha response.
Instead of accessing the DOM directly with document.getElementsByName
, consider using the hCaptcha component's methods or events to get the response value. The Vue ref is already available as this.hcaptcha
.
-this.form['h-captcha-response'] = document.getElementsByName('h-captcha-response')[0].value
+// Use the hCaptcha component's execute method to get the response
+const response = await this.hcaptcha.execute()
+this.form['h-captcha-response'] = response
Committable suggestion skipped: line range outside the PR's diff.
@@ -27,6 +28,10 @@ class RegisterController extends Controller | |||
public function __construct() | |||
{ | |||
$this->middleware('guest'); | |||
if (app()->environment() !== 'testing') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason why this is in this if?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work, small comments added thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Outside diff range and nitpick comments (3)
api/app/Integrations/Handlers/EmailIntegration.php (2)
35-40
: Strengthen email validation for spam prevention.The validation for multiple email addresses is good, but consider these enhancements:
- Add input sanitization for the email addresses
- Consider adding rate limiting for email sending
$rules['send_to'][] = function ($attribute, $value, $fail) use ($form) { + $value = trim(strip_tags($value)); if (count(explode("\n", trim($value))) > 1 || count(explode(',', $value)) > 1) { $fail('You can only send to a single email address on the free plan. Please upgrade to the Pro plan to create a new integration.'); } + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + $fail('Please enter a valid email address.'); + } };
Line range hint
1-140
: Good security implementation with layered defenses.The implementation provides multiple layers of security:
- Input validation for free users
- Integration limits
- Risk-based restrictions
- Pro feature gating
Consider implementing these architectural improvements:
- Add rate limiting at the API level
- Implement email domain validation/blacklisting
- Add monitoring for abuse patterns
api/tests/Feature/RegisterTest.php (1)
Line range hint
32-42
: Consider adding negative test cases for hCaptcha validation.While the test covers duplicate email validation, consider adding test cases for:
- Invalid hCaptcha token
- Missing hCaptcha token
- Failed hCaptcha verification
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
api/app/Http/Controllers/Auth/RegisterController.php
(4 hunks)api/app/Integrations/Handlers/EmailIntegration.php
(2 hunks)api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php
(1 hunks)api/tests/Feature/RegisterTest.php
(3 hunks)client/components/pages/auth/components/RegisterForm.vue
(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php
🔇 Additional comments (12)
api/app/Integrations/Handlers/EmailIntegration.php (3)
5-12
: LGTM! Dependencies are properly imported.
The new imports for Form
model and ValidationException
are necessary for the added validation logic.
18-18
: LGTM! Pro plan check is properly implemented.
The validation rules correctly bypass restrictions for pro users and self-hosted installations.
Also applies to: 31-33
16-16
: Verify risk limit effectiveness.
The risk limit check provides an additional layer of security, but let's verify its effectiveness:
Also applies to: 77-89
✅ Verification successful
Risk limit implementation is effective and properly enforced
The verification shows a robust security implementation:
- Risk assessment is cached for 15 minutes via
remember()
method - Workspace-level risk check evaluates all owners' risk status
- Email sending is blocked when risky workspace reaches the 120 submissions limit
- Suspicious activity is properly logged with workspace and form identifiers
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check for any bypass attempts or related security issues
rg -A 5 "RISKY_USERS_LIMIT|is_risky" --type php
# Check for any email-related security incidents in logs
rg -A 5 "!!!DANGER!!!" --type php
Length of output: 3650
api/tests/Feature/RegisterTest.php (3)
8-28
: LGTM! Comprehensive test coverage for successful registration.
The test properly mocks hCaptcha verification and validates the registration flow including IP address storage.
Line range hint 44-73
: LGTM! Comprehensive validation for disposable emails.
The test properly validates the rejection of disposable emails and includes proper error message verification.
75-87
: LGTM! Good coverage of environment-specific validation.
The test properly validates that hCaptcha is required in production environment.
api/app/Http/Controllers/Auth/RegisterController.php (3)
64-81
: LGTM! Comprehensive validation rules with proper hCaptcha integration.
The validation rules are well-structured and the conditional hCaptcha validation is properly implemented.
31-34
:
Consider implementing stricter rate limits for registration.
While multiple rate limits are good, the current limits (5/min, 30/hour) might be too permissive for a registration endpoint.
Apply this diff to implement stricter limits:
- if (app()->environment() !== 'testing') {
- $this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute
- $this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour
+ if (app()->environment() !== 'testing') {
+ $this->middleware('throttle:3,60')->only('register'); // 3 attempts per hour
+ }
98-98
:
Review IP address storage for privacy compliance.
Storing IP addresses has privacy implications and might be subject to data protection regulations (GDPR, CCPA).
Apply this diff to implement IP hashing:
- 'meta' => ['registration_ip' => request()->ip()],
+ 'meta' => ['registration_ip' => hash('sha256', request()->ip())],
client/components/pages/auth/components/RegisterForm.vue (3)
55-68
: LGTM! Clean hCaptcha component integration.
The hCaptcha component is properly integrated with conditional rendering and error handling.
179-189
: LGTM! Well-structured form data with proper computed properties.
The form data is well-organized and the computed property for the hCaptcha site key is properly implemented.
237-240
: 🛠️ Refactor suggestion
Avoid direct DOM access for hCaptcha response.
Instead of accessing the DOM directly with document.getElementsByName
, use the hCaptcha component's methods or events.
Apply this diff to use the component's methods:
if (this.hCaptchaSiteKey) {
- this.form['h-captcha-response'] = document.getElementsByName('h-captcha-response')[0].value
+ const response = await this.hcaptcha.execute()
+ this.form['h-captcha-response'] = response
this.hcaptcha.reset()
}
// Free plan users can only have a single email integration per form (avoid spam) | ||
if (!request()->route('integrationid')) { | ||
$existingEmailIntegrations = FormIntegration::where('form_id', $form->id) | ||
->where('integration_id', 'email') | ||
->count(); | ||
if ($existingEmailIntegrations > 0) { | ||
throw ValidationException::withMessages([ | ||
'settings.send_to' => ['Free users are limited to 1 email integration per form.'] | ||
]); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider edge cases in integration validation.
The validation for existing integrations should:
- Consider soft-deleted integrations
- Use consistent error messages
if (!request()->route('integrationid')) {
$existingEmailIntegrations = FormIntegration::where('form_id', $form->id)
->where('integration_id', 'email')
+ ->whereNull('deleted_at') // Exclude soft-deleted integrations
->count();
if ($existingEmailIntegrations > 0) {
throw ValidationException::withMessages([
- 'settings.send_to' => ['Free users are limited to 1 email integration per form.']
+ 'settings.send_to' => ['You can only have a single email integration on the free plan. Please upgrade to the Pro plan to create a new integration.']
]);
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Free plan users can only have a single email integration per form (avoid spam) | |
if (!request()->route('integrationid')) { | |
$existingEmailIntegrations = FormIntegration::where('form_id', $form->id) | |
->where('integration_id', 'email') | |
->count(); | |
if ($existingEmailIntegrations > 0) { | |
throw ValidationException::withMessages([ | |
'settings.send_to' => ['Free users are limited to 1 email integration per form.'] | |
]); | |
} | |
} | |
// Free plan users can only have a single email integration per form (avoid spam) | |
if (!request()->route('integrationid')) { | |
$existingEmailIntegrations = FormIntegration::where('form_id', $form->id) | |
->where('integration_id', 'email') | |
->whereNull('deleted_at') // Exclude soft-deleted integrations | |
->count(); | |
if ($existingEmailIntegrations > 0) { | |
throw ValidationException::withMessages([ | |
'settings.send_to' => ['You can only have a single email integration on the free plan. Please upgrade to the Pro plan to create a new integration.'] | |
]); | |
} | |
} |
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation
Chores
meta
field in the users table.