-
Notifications
You must be signed in to change notification settings - Fork 319
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
Implement regex validation conditions in form logic #645
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -306,6 +306,20 @@ private function textConditionMet(array $propertyCondition, $value): bool | |
return $this->checkLength($propertyCondition, $value, '<'); | ||
case 'content_length_less_than_or_equal_to': | ||
return $this->checkLength($propertyCondition, $value, '<='); | ||
case 'matches_regex': | ||
try { | ||
ray('matches_regex', $propertyCondition['value'], $value); | ||
return (bool) preg_match('/' . $propertyCondition['value'] . '/', $value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix potential regex injection vulnerability. Direct concatenation of user input into regex patterns is unsafe. The pattern should be properly escaped and validated:
- return (bool) preg_match('/' . $propertyCondition['value'] . '/', $value);
+ $pattern = $propertyCondition['value'];
+ if (!$this->isValidRegexPattern($pattern)) {
+ throw new \InvalidArgumentException('Invalid regex pattern');
+ }
+ $escaped_pattern = '/' . str_replace('/', '\/', $pattern) . '/';
+ return (bool) preg_match($escaped_pattern, $value); Add this helper method: private function isValidRegexPattern(string $pattern): bool
{
try {
// Test compile the pattern
return @preg_match('/' . str_replace('/', '\/', $pattern) . '/', '') !== false;
} catch (\Exception $e) {
return false;
}
} Also applies to: 319-319 |
||
} catch (\Exception $e) { | ||
ray('matches_regex_error', $e); | ||
return false; | ||
} | ||
Comment on lines
+312
to
+315
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Standardize error handling behavior. The error handling differs between } catch (\Exception $e) {
- ray('matches_regex_error', $e);
- return false;
+ throw new \InvalidArgumentException('Invalid regex pattern: ' . $e->getMessage());
}
case 'does_not_match_regex':
try {
return !(bool) preg_match('/' . $propertyCondition['value'] . '/', $value);
} catch (\Exception $e) {
- return true;
+ throw new \InvalidArgumentException('Invalid regex pattern: ' . $e->getMessage());
} Also applies to: 320-322 |
||
case 'does_not_match_regex': | ||
try { | ||
return !(bool) preg_match('/' . $propertyCondition['value'] . '/', $value); | ||
} catch (\Exception $e) { | ||
return true; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add performance safeguards for regex evaluation. Consider adding safeguards against catastrophic backtracking:
private function evaluateRegexPattern(string $pattern, string $value, int $timeout_ms = 100): bool
{
// Set timeout
ini_set('pcre.backtrack_limit', '1000000');
ini_set('pcre.jit', '0');
// Use custom error handler to catch timeouts
set_error_handler(function($errno, $errstr) {
throw new \RuntimeException('Regex evaluation timeout');
}, E_WARNING);
try {
$escaped_pattern = '/' . str_replace('/', '\/', $pattern) . '/';
return (bool) preg_match($escaped_pattern, $value);
} finally {
restore_error_handler();
}
} |
||
} | ||
|
||
return false; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -228,6 +228,46 @@ | |
}, | ||
"content_length_less_than_or_equal_to": { | ||
"expected_type": "number" | ||
}, | ||
"matches_regex": { | ||
"expected_type": "string", | ||
"format": { | ||
"type": "regex" | ||
} | ||
}, | ||
"does_not_match_regex": { | ||
"expected_type": "string", | ||
"format": { | ||
"type": "regex" | ||
} | ||
} | ||
} | ||
}, | ||
"matrix": { | ||
"comparators": { | ||
"equals": { | ||
"expected_type": "object", | ||
"format": { | ||
"type": "object" | ||
} | ||
}, | ||
"does_not_equal": { | ||
"expected_type": "object", | ||
"format": { | ||
"type":"object" | ||
} | ||
}, | ||
"contains": { | ||
"expected_type": "object", | ||
"format": { | ||
"type":"object" | ||
} | ||
}, | ||
"does_not_contain": { | ||
"expected_type": "object", | ||
"format": { | ||
"type":"object" | ||
} | ||
Comment on lines
+246
to
+270
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Define schema for matrix object structure The matrix comparators lack a schema definition for the expected object structure, which could lead to validation inconsistencies. Consider adding a schema definition: "matrix": {
"comparators": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "columns": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["rows", "columns"]
+ },
"equals": {
"expected_type": "object",
"format": {
"type": "object"
+ "ref": "#/comparators/schema"
}
},
// Apply similar changes to other comparators
|
||
} | ||
} | ||
}, | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,7 @@ | ||||||||||||||||||
<?php | ||||||||||||||||||
|
||||||||||||||||||
use Illuminate\Testing\Fluent\AssertableJson; | ||||||||||||||||||
use Tests\Helpers\FormSubmissionDataFactory; | ||||||||||||||||||
|
||||||||||||||||||
it('create form with logic', function () { | ||||||||||||||||||
$user = $this->actingAsUser(); | ||||||||||||||||||
|
@@ -348,3 +349,198 @@ | |||||||||||||||||
] | ||||||||||||||||||
]); | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
|
||||||||||||||||||
it('can submit form with passed regex validation condition', function () { | ||||||||||||||||||
$user = $this->actingAsUser(); | ||||||||||||||||||
$workspace = $this->createUserWorkspace($user); | ||||||||||||||||||
$form = $this->createForm($user, $workspace); | ||||||||||||||||||
$targetField = collect($form->properties)->where('name', 'Email')->first(); | ||||||||||||||||||
|
||||||||||||||||||
// Regex condition to check if email is from gmail.com domain | ||||||||||||||||||
$condition = [ | ||||||||||||||||||
'actions' => [], | ||||||||||||||||||
'conditions' => [ | ||||||||||||||||||
'operatorIdentifier' => 'and', | ||||||||||||||||||
'children' => [ | ||||||||||||||||||
[ | ||||||||||||||||||
'identifier' => $targetField['id'], | ||||||||||||||||||
'value' => [ | ||||||||||||||||||
'operator' => 'matches_regex', | ||||||||||||||||||
'property_meta' => [ | ||||||||||||||||||
'id' => $targetField['id'], | ||||||||||||||||||
'type' => 'text', | ||||||||||||||||||
], | ||||||||||||||||||
'value' => '^[a-zA-Z0-9._%+-]+@gmail\.com$', | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
]; | ||||||||||||||||||
|
||||||||||||||||||
$submissionData = []; | ||||||||||||||||||
$validationMessage = 'Must be a Gmail address'; | ||||||||||||||||||
|
||||||||||||||||||
$form->properties = collect($form->properties)->map(function ($property) use (&$submissionData, &$condition, &$validationMessage, $targetField) { | ||||||||||||||||||
if (in_array($property['name'], ['Name'])) { | ||||||||||||||||||
$property['validation'] = ['error_conditions' => $condition, 'error_message' => $validationMessage]; | ||||||||||||||||||
$submissionData[$targetField['id']] = '[email protected]'; | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+385
to
+388
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix field mapping in validation setup. The validation is being applied to the 'Name' field but using the 'Email' field's value, which is confusing and could lead to maintenance issues. Apply this diff to fix the field mapping: -if (in_array($property['name'], ['Name'])) {
+if (in_array($property['name'], ['Email'])) { 📝 Committable suggestion
Suggested change
|
||||||||||||||||||
return $property; | ||||||||||||||||||
})->toArray(); | ||||||||||||||||||
|
||||||||||||||||||
$form->update(); | ||||||||||||||||||
$formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData); | ||||||||||||||||||
|
||||||||||||||||||
$response = $this->postJson(route('forms.answer', $form->slug), $formData); | ||||||||||||||||||
$response->assertSuccessful() | ||||||||||||||||||
->assertJson([ | ||||||||||||||||||
'type' => 'success', | ||||||||||||||||||
'message' => 'Form submission saved.', | ||||||||||||||||||
]); | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
it('can not submit form with failed regex validation condition', function () { | ||||||||||||||||||
$user = $this->actingAsUser(); | ||||||||||||||||||
$workspace = $this->createUserWorkspace($user); | ||||||||||||||||||
$form = $this->createForm($user, $workspace); | ||||||||||||||||||
$targetField = collect($form->properties)->where('name', 'Email')->first(); | ||||||||||||||||||
|
||||||||||||||||||
// Regex condition to check if email is from gmail.com domain | ||||||||||||||||||
$condition = [ | ||||||||||||||||||
'actions' => [], | ||||||||||||||||||
'conditions' => [ | ||||||||||||||||||
'operatorIdentifier' => 'and', | ||||||||||||||||||
'children' => [ | ||||||||||||||||||
[ | ||||||||||||||||||
'identifier' => $targetField['id'], | ||||||||||||||||||
'value' => [ | ||||||||||||||||||
'operator' => 'matches_regex', | ||||||||||||||||||
'property_meta' => [ | ||||||||||||||||||
'id' => $targetField['id'], | ||||||||||||||||||
'type' => 'text', | ||||||||||||||||||
], | ||||||||||||||||||
'value' => '^[a-zA-Z0-9._%+-]+@gmail\.com$', | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
]; | ||||||||||||||||||
|
||||||||||||||||||
$submissionData = []; | ||||||||||||||||||
$validationMessage = 'Must be a Gmail address'; | ||||||||||||||||||
|
||||||||||||||||||
$form->properties = collect($form->properties)->map(function ($property) use (&$submissionData, &$condition, &$validationMessage, $targetField) { | ||||||||||||||||||
if (in_array($property['name'], ['Name'])) { | ||||||||||||||||||
$property['validation'] = ['error_conditions' => $condition, 'error_message' => $validationMessage]; | ||||||||||||||||||
$submissionData[$targetField['id']] = '[email protected]'; // Non-Gmail address should fail | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+434
to
+437
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix field mapping in validation setup. The validation is being applied to the 'Name' field but using the 'Email' field's value. Apply this diff to fix the field mapping: -if (in_array($property['name'], ['Name'])) {
+if (in_array($property['name'], ['Email'])) { 📝 Committable suggestion
Suggested change
|
||||||||||||||||||
return $property; | ||||||||||||||||||
})->toArray(); | ||||||||||||||||||
|
||||||||||||||||||
$form->update(); | ||||||||||||||||||
$formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData); | ||||||||||||||||||
|
||||||||||||||||||
$this->postJson(route('forms.answer', $form->slug), $formData) | ||||||||||||||||||
->assertStatus(422) | ||||||||||||||||||
->assertJson([ | ||||||||||||||||||
'message' => $validationMessage, | ||||||||||||||||||
]); | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
it('can submit form with does not match regex validation condition', function () { | ||||||||||||||||||
$user = $this->actingAsUser(); | ||||||||||||||||||
$workspace = $this->createUserWorkspace($user); | ||||||||||||||||||
$form = $this->createForm($user, $workspace); | ||||||||||||||||||
$targetField = collect($form->properties)->where('name', 'Email')->first(); | ||||||||||||||||||
|
||||||||||||||||||
// Regex condition to check if email is NOT from gmail.com domain | ||||||||||||||||||
$condition = [ | ||||||||||||||||||
'actions' => [], | ||||||||||||||||||
'conditions' => [ | ||||||||||||||||||
'operatorIdentifier' => 'and', | ||||||||||||||||||
'children' => [ | ||||||||||||||||||
[ | ||||||||||||||||||
'identifier' => $targetField['id'], | ||||||||||||||||||
'value' => [ | ||||||||||||||||||
'operator' => 'does_not_match_regex', | ||||||||||||||||||
'property_meta' => [ | ||||||||||||||||||
'id' => $targetField['id'], | ||||||||||||||||||
'type' => 'text', | ||||||||||||||||||
], | ||||||||||||||||||
'value' => '^[a-zA-Z0-9._%+-]+@gmail\.com$', | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
]; | ||||||||||||||||||
|
||||||||||||||||||
$submissionData = []; | ||||||||||||||||||
$validationMessage = 'Gmail addresses not allowed'; | ||||||||||||||||||
|
||||||||||||||||||
$form->properties = collect($form->properties)->map(function ($property) use (&$submissionData, &$condition, &$validationMessage, $targetField) { | ||||||||||||||||||
if (in_array($property['name'], ['Name'])) { | ||||||||||||||||||
$property['validation'] = ['error_conditions' => $condition, 'error_message' => $validationMessage]; | ||||||||||||||||||
$submissionData[$targetField['id']] = '[email protected]'; // Non-Gmail address should pass | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+482
to
+485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix field mapping in validation setup. The validation is being applied to the 'Name' field but using the 'Email' field's value. Apply this diff to fix the field mapping: -if (in_array($property['name'], ['Name'])) {
+if (in_array($property['name'], ['Email'])) { 📝 Committable suggestion
Suggested change
|
||||||||||||||||||
return $property; | ||||||||||||||||||
})->toArray(); | ||||||||||||||||||
|
||||||||||||||||||
$form->update(); | ||||||||||||||||||
$formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData); | ||||||||||||||||||
|
||||||||||||||||||
$response = $this->postJson(route('forms.answer', $form->slug), $formData); | ||||||||||||||||||
$response->assertSuccessful() | ||||||||||||||||||
->assertJson([ | ||||||||||||||||||
'type' => 'success', | ||||||||||||||||||
'message' => 'Form submission saved.', | ||||||||||||||||||
]); | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
it('handles invalid regex patterns gracefully', function () { | ||||||||||||||||||
$user = $this->actingAsUser(); | ||||||||||||||||||
$workspace = $this->createUserWorkspace($user); | ||||||||||||||||||
$form = $this->createForm($user, $workspace); | ||||||||||||||||||
$targetField = collect($form->properties)->where('name', 'Email')->first(); | ||||||||||||||||||
|
||||||||||||||||||
// Invalid regex pattern | ||||||||||||||||||
$condition = [ | ||||||||||||||||||
'actions' => [], | ||||||||||||||||||
'conditions' => [ | ||||||||||||||||||
'operatorIdentifier' => 'and', | ||||||||||||||||||
'children' => [ | ||||||||||||||||||
[ | ||||||||||||||||||
'identifier' => $targetField['id'], | ||||||||||||||||||
'value' => [ | ||||||||||||||||||
'operator' => 'matches_regex', | ||||||||||||||||||
'property_meta' => [ | ||||||||||||||||||
'id' => $targetField['id'], | ||||||||||||||||||
'type' => 'text', | ||||||||||||||||||
], | ||||||||||||||||||
'value' => '[Invalid Regex)', // Invalid regex pattern | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
], | ||||||||||||||||||
]; | ||||||||||||||||||
|
||||||||||||||||||
$submissionData = []; | ||||||||||||||||||
$validationMessage = 'Invalid regex pattern'; | ||||||||||||||||||
|
||||||||||||||||||
$form->properties = collect($form->properties)->map(function ($property) use (&$submissionData, &$condition, &$validationMessage, $targetField) { | ||||||||||||||||||
if (in_array($property['name'], ['Name'])) { | ||||||||||||||||||
$property['validation'] = ['error_conditions' => $condition, 'error_message' => $validationMessage]; | ||||||||||||||||||
$submissionData[$targetField['id']] = '[email protected]'; | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+531
to
+534
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix field mapping in validation setup. The validation is being applied to the 'Name' field but using the 'Email' field's value. Apply this diff to fix the field mapping: -if (in_array($property['name'], ['Name'])) {
+if (in_array($property['name'], ['Email'])) { 📝 Committable suggestion
Suggested change
|
||||||||||||||||||
return $property; | ||||||||||||||||||
})->toArray(); | ||||||||||||||||||
|
||||||||||||||||||
$form->update(); | ||||||||||||||||||
$formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData); | ||||||||||||||||||
|
||||||||||||||||||
$this->postJson(route('forms.answer', $form->slug), $formData) | ||||||||||||||||||
->assertStatus(422) | ||||||||||||||||||
->assertJson([ | ||||||||||||||||||
'message' => $validationMessage, | ||||||||||||||||||
]); | ||||||||||||||||||
}); |
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.
Improve regex pattern validation for better security and reliability.
The current implementation has potential security issues and might not catch all invalid patterns.
Consider these improvements:
preg_match_all
with a limit to prevent catastrophic backtracking: