fix(validation): make hostname validation case-insensitive and expand allowed name characters (#9134)
This commit is contained in:
commit
45114c8165
5 changed files with 106 additions and 11 deletions
|
|
@ -62,12 +62,15 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
|||
// Ignore errors when facades are not available (e.g., in unit tests)
|
||||
}
|
||||
|
||||
$fail('The :attribute contains invalid characters. Only lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
|
||||
$fail('The :attribute contains invalid characters. Only letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize to lowercase for validation (RFC 1123 hostnames are case-insensitive)
|
||||
$hostname = strtolower($hostname);
|
||||
|
||||
// Additional validation: hostname should not start or end with a dot
|
||||
if (str_starts_with($hostname, '.') || str_ends_with($hostname, '.')) {
|
||||
$fail('The :attribute cannot start or end with a dot.');
|
||||
|
|
@ -100,9 +103,9 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if label contains only valid characters (lowercase letters, digits, hyphens)
|
||||
// Check if label contains only valid characters (letters, digits, hyphens)
|
||||
if (! preg_match('/^[a-z0-9-]+$/', $label)) {
|
||||
$fail('The :attribute contains invalid characters. Only lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
|
||||
$fail('The :attribute contains invalid characters. Only letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ValidationPatterns
|
|||
/**
|
||||
* Pattern for names excluding all dangerous characters
|
||||
*/
|
||||
public const NAME_PATTERN = '/^[\p{L}\p{M}\p{N}\s\-_.@\/&]+$/u';
|
||||
public const NAME_PATTERN = '/^[\p{L}\p{M}\p{N}\s\-_.@\/&()#,:+]+$/u';
|
||||
|
||||
/**
|
||||
* Pattern for descriptions excluding all dangerous characters with some additional allowed characters
|
||||
|
|
@ -96,7 +96,7 @@ public static function descriptionRules(bool $required = false, int $maxLength =
|
|||
public static function nameMessages(): array
|
||||
{
|
||||
return [
|
||||
'name.regex' => 'The name may only contain letters (including Unicode), numbers, spaces, and these characters: - _ . / @ &',
|
||||
'name.regex' => 'The name may only contain letters (including Unicode), numbers, spaces, and these characters: - _ . / @ & ( ) # , : +',
|
||||
'name.min' => 'The name must be at least :min characters.',
|
||||
'name.max' => 'The name may not be greater than :max characters.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -341,7 +341,16 @@ function generate_application_name(string $git_repository, string $git_branch, ?
|
|||
|
||||
$repo_name = str_contains($git_repository, '/') ? last(explode('/', $git_repository)) : $git_repository;
|
||||
|
||||
return Str::kebab("$repo_name:$git_branch-$cuid");
|
||||
$name = Str::kebab("$repo_name:$git_branch-$cuid");
|
||||
|
||||
// Strip characters not allowed by NAME_PATTERN
|
||||
$name = preg_replace('/[^\p{L}\p{M}\p{N}\s\-_.@\/&()#,:+]+/u', '', $name);
|
||||
|
||||
if (empty($name) || mb_strlen($name) < 3) {
|
||||
return generate_random_name($cuid);
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
'subdomain' => 'web.app.example.com',
|
||||
'max label length' => str_repeat('a', 63),
|
||||
'max total length' => str_repeat('a', 63).'.'.str_repeat('b', 63).'.'.str_repeat('c', 63).'.'.str_repeat('d', 59),
|
||||
'uppercase hostname' => 'MyServer',
|
||||
'mixed case fqdn' => 'MyServer.Example.COM',
|
||||
]);
|
||||
|
||||
it('rejects invalid RFC 1123 hostnames', function (string $hostname, string $expectedError) {
|
||||
|
|
@ -36,8 +38,7 @@
|
|||
expect($failCalled)->toBeTrue();
|
||||
expect($errorMessage)->toContain($expectedError);
|
||||
})->with([
|
||||
'uppercase letters' => ['MyServer', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
'underscore' => ['my_server', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
'underscore' => ['my_server', 'letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
'starts with hyphen' => ['-myserver', 'cannot start or end with a hyphen'],
|
||||
'ends with hyphen' => ['myserver-', 'cannot start or end with a hyphen'],
|
||||
'starts with dot' => ['.myserver', 'cannot start or end with a dot'],
|
||||
|
|
@ -46,9 +47,9 @@
|
|||
'too long total' => [str_repeat('a', 254), 'must not exceed 253 characters'],
|
||||
'label too long' => [str_repeat('a', 64), 'must be 1-63 characters'],
|
||||
'empty label' => ['my..server', 'consecutive dots'],
|
||||
'special characters' => ['my@server', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
'space' => ['my server', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
'shell metacharacters' => ['my;server', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
'special characters' => ['my@server', 'letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
'space' => ['my server', 'letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
'shell metacharacters' => ['my;server', 'letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.)'],
|
||||
]);
|
||||
|
||||
it('accepts empty hostname', function () {
|
||||
|
|
|
|||
82
tests/Unit/ValidationPatternsTest.php
Normal file
82
tests/Unit/ValidationPatternsTest.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
use App\Support\ValidationPatterns;
|
||||
|
||||
it('accepts valid names with common characters', function (string $name) {
|
||||
expect(preg_match(ValidationPatterns::NAME_PATTERN, $name))->toBe(1);
|
||||
})->with([
|
||||
'simple name' => 'My Server',
|
||||
'name with hyphen' => 'my-server',
|
||||
'name with underscore' => 'my_server',
|
||||
'name with dot' => 'my.server',
|
||||
'name with slash' => 'my/server',
|
||||
'name with at sign' => 'user@host',
|
||||
'name with ampersand' => 'Tom & Jerry',
|
||||
'name with parentheses' => 'My Server (Production)',
|
||||
'name with hash' => 'Server #1',
|
||||
'name with comma' => 'Server, v2',
|
||||
'name with colon' => 'Server: Production',
|
||||
'name with plus' => 'C++ App',
|
||||
'unicode name' => 'Ünïcödé Sërvér',
|
||||
'unicode chinese' => '我的服务器',
|
||||
'numeric name' => '12345',
|
||||
'complex name' => 'App #3 (staging): v2.1+hotfix',
|
||||
]);
|
||||
|
||||
it('rejects names with dangerous characters', function (string $name) {
|
||||
expect(preg_match(ValidationPatterns::NAME_PATTERN, $name))->toBe(0);
|
||||
})->with([
|
||||
'semicolon' => 'my;server',
|
||||
'pipe' => 'my|server',
|
||||
'dollar sign' => 'my$server',
|
||||
'backtick' => 'my`server',
|
||||
'backslash' => 'my\\server',
|
||||
'less than' => 'my<server',
|
||||
'greater than' => 'my>server',
|
||||
'curly braces' => 'my{server}',
|
||||
'square brackets' => 'my[server]',
|
||||
'tilde' => 'my~server',
|
||||
'caret' => 'my^server',
|
||||
'question mark' => 'my?server',
|
||||
'percent' => 'my%server',
|
||||
'double quote' => 'my"server',
|
||||
'exclamation' => 'my!server',
|
||||
'asterisk' => 'my*server',
|
||||
]);
|
||||
|
||||
it('generates nameRules with correct defaults', function () {
|
||||
$rules = ValidationPatterns::nameRules();
|
||||
|
||||
expect($rules)->toContain('required')
|
||||
->toContain('string')
|
||||
->toContain('min:3')
|
||||
->toContain('max:255')
|
||||
->toContain('regex:'.ValidationPatterns::NAME_PATTERN);
|
||||
});
|
||||
|
||||
it('generates nullable nameRules when not required', function () {
|
||||
$rules = ValidationPatterns::nameRules(required: false);
|
||||
|
||||
expect($rules)->toContain('nullable')
|
||||
->not->toContain('required');
|
||||
});
|
||||
|
||||
it('generates application names that comply with NAME_PATTERN', function (string $repo, string $branch) {
|
||||
$name = generate_application_name($repo, $branch, 'testcuid');
|
||||
|
||||
expect(preg_match(ValidationPatterns::NAME_PATTERN, $name))->toBe(1);
|
||||
})->with([
|
||||
'normal repo' => ['owner/my-app', 'main'],
|
||||
'repo with dots' => ['repo.with.dots', 'feat/branch'],
|
||||
'repo with plus' => ['C++ App', 'main'],
|
||||
'branch with parens' => ['my-app', 'fix(auth)-login'],
|
||||
'repo with exclamation' => ['my-app!', 'main'],
|
||||
'repo with brackets' => ['app[test]', 'develop'],
|
||||
]);
|
||||
|
||||
it('falls back to random name when repo produces empty name', function () {
|
||||
$name = generate_application_name('!!!', 'main', 'testcuid');
|
||||
|
||||
expect(mb_strlen($name))->toBeGreaterThanOrEqual(3)
|
||||
->and(preg_match(ValidationPatterns::NAME_PATTERN, $name))->toBe(1);
|
||||
});
|
||||
Loading…
Reference in a new issue