2025-08-19 10:14:48 +00:00
< ? php
namespace App\Support ;
/**
* Shared validation patterns for consistent use across the application
*/
class ValidationPatterns
{
/**
2026-01-05 12:14:27 +00:00
* Pattern for names excluding all dangerous characters
2026-03-18 12:53:01 +00:00
*/
2026-03-24 07:03:08 +00:00
public const NAME_PATTERN = '/^[\p{L}\p{M}\p{N}\s\-_.@\/&()#,:+]+$/u' ;
2025-08-19 10:14:48 +00:00
/**
2026-01-05 12:14:27 +00:00
* Pattern for descriptions excluding all dangerous characters with some additional allowed characters
2025-08-19 10:14:48 +00:00
*/
2026-01-19 17:50:56 +00:00
public const DESCRIPTION_PATTERN = '/^[\p{L}\p{M}\p{N}\s\-_.,!?()\'\"+=*@\/&]+$/u' ;
2025-08-19 10:14:48 +00:00
2026-03-12 12:09:13 +00:00
/**
* Pattern for file paths ( dockerfile location , docker compose location , etc . )
* Allows alphanumeric , dots , hyphens , underscores , slashes , @ , ~ , and +
*/
public const FILE_PATH_PATTERN = '/^\/[a-zA-Z0-9._\-\/~@+]+$/' ;
2026-03-18 12:53:01 +00:00
/**
* Pattern for directory paths ( base_directory , publish_directory , etc . )
* Like FILE_PATH_PATTERN but also allows bare " / " ( root directory )
*/
public const DIRECTORY_PATH_PATTERN = '/^\/([a-zA-Z0-9._\-\/~@+]*)?$/' ;
/**
* Pattern for Docker build target names ( multi - stage build stage names )
* Allows alphanumeric , dots , hyphens , and underscores
*/
public const DOCKER_TARGET_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/' ;
/**
refactor(validation): tokenize shell-safe command pattern
Replace the flat character-class regex for SHELL_SAFE_COMMAND_PATTERN with
a token-aware alternation. The parser now recognizes explicit tokens
(`&&`, `||`, balanced single/double quotes, whitespace, and an unquoted
safe-char run) instead of a bag of characters, which lets us extend the
accepted grammar without loosening the guarantees.
New surface area, with tests:
- logical OR chaining (`make build || make clean`)
- shell globs and bang (`rm *.tmp`, `cp src/?.js dist/`, `! grep -q foo`)
- single-quoted arguments are now treated as balanced runs rather than
rejected per-character
Preserved surface area:
- && chaining, balanced "..." and '...' quotes, the previous safe path /
argument characters, and the existing error-path contract in
ApplicationDeploymentJob::validateShellSafeCommand().
Also refreshes the user-facing validation messages in General.php so the
allow/deny list shown on failure matches the new grammar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 20:00:41 +00:00
* Token - aware pattern for shell - safe command strings ( docker compose commands , docker run options ) .
*
* Accepts a sequence of the following tokens only :
* [ \t ] + — whitespace ( space / tab )
* && — logical AND ( matched before bare & can match anything )
* || — logical OR ( matched before bare | can match anything )
* " [^ " $ ` \\\n\r ] * " — balanced double-quoted string; blocks $ , backtick, \ , newlines inside
* '[^' \n\r ] * ' — balanced single - quoted string ; blocks newlines inside ( all else literal )
* [ safe - chars ] + — unquoted alphanumerics + safe path / arg chars ( includes glob * , ? , and ! )
*
* Blocked everywhere ( outside and inside unquoted tokens ) :
* bare & ( background op ), bare | , ;, $ , ` , (, ), < , > , \ , newline , CR
*
* Blocked inside double - quoted spans specifically :
* $ ( variable / command expansion ), ` ( command substitution ), \ ( escape )
*
* Legitimate use cases preserved :
* docker compose build && docker tag x && docker push y
* make build || make clean
* rm *. tmp cp src / ? . js dist /
* ! grep - q foo && echo missing
* docker compose up - d -- build - arg VERSION = " 1.0.0 "
* -- entrypoint " sh -c 'npm start' "
2026-03-18 12:53:01 +00:00
*/
refactor(validation): tokenize shell-safe command pattern
Replace the flat character-class regex for SHELL_SAFE_COMMAND_PATTERN with
a token-aware alternation. The parser now recognizes explicit tokens
(`&&`, `||`, balanced single/double quotes, whitespace, and an unquoted
safe-char run) instead of a bag of characters, which lets us extend the
accepted grammar without loosening the guarantees.
New surface area, with tests:
- logical OR chaining (`make build || make clean`)
- shell globs and bang (`rm *.tmp`, `cp src/?.js dist/`, `! grep -q foo`)
- single-quoted arguments are now treated as balanced runs rather than
rejected per-character
Preserved surface area:
- && chaining, balanced "..." and '...' quotes, the previous safe path /
argument characters, and the existing error-path contract in
ApplicationDeploymentJob::validateShellSafeCommand().
Also refreshes the user-facing validation messages in General.php so the
allow/deny list shown on failure matches the new grammar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 20:00:41 +00:00
public const SHELL_SAFE_COMMAND_PATTERN = '/^(?:[ \t]+|&&|\|\||"[^"$`\\\\\n\r]*"|\'[^\'\n\r]*\'|[a-zA-Z0-9._\-\/=:@,+\[\]{}#%^~*?!]+)+$/' ;
2026-03-18 12:53:01 +00:00
2026-03-26 10:06:30 +00:00
/**
* Pattern for Docker volume names
* Must start with alphanumeric , followed by alphanumeric , dots , hyphens , or underscores
* Matches Docker ' s volume naming rules
*/
public const VOLUME_NAME_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/' ;
2026-03-18 12:53:01 +00:00
/**
* Pattern for Docker container names
* Must start with alphanumeric , followed by alphanumeric , dots , hyphens , or underscores
*/
public const CONTAINER_NAME_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/' ;
2026-03-28 11:28:59 +00:00
/**
* Pattern for Docker network names
* Must start with alphanumeric , followed by alphanumeric , dots , hyphens , or underscores
* Matches Docker ' s network naming rules and prevents shell injection
*/
public const DOCKER_NETWORK_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/' ;
2026-04-20 11:58:36 +00:00
/**
* Pattern for SQL - safe unquoted database identifiers ( usernames , database names ) .
* Allows letters , digits , underscore ; first char must be letter or underscore .
* Excludes all shell metacharacters . Max 63 chars ( Postgres identifier limit ) .
*/
public const DB_IDENTIFIER_PATTERN = '/^[A-Za-z_][A-Za-z0-9_]{0,62}$/' ;
/**
* Pattern for database passwords .
* Excludes shell - dangerous characters : backtick , $ , ;, | , & , < , > , \ , ' , " , space, newline, CR, tab, null.
* Allows a broad set of printable characters so passwords remain strong .
*/
public const DB_PASSWORD_PATTERN = '/^[A-Za-z0-9!@#%^*()_+\-=\[\]{}:,.?\/~]+$/' ;
/**
* Get validation rules for database identifier fields ( username , database name ) .
2026-04-20 11:45:57 +00:00
*
* Set $enforcePattern to false to skip the regex check ( for example when
* re - validating a legacy value on an existing record that has not been
* changed by the user ) . The length and type rules are always applied .
2026-04-20 11:58:36 +00:00
*/
2026-04-20 11:45:57 +00:00
public static function databaseIdentifierRules ( bool $required = true , int $minLength = 1 , int $maxLength = 63 , bool $enforcePattern = true ) : array
2026-04-20 11:58:36 +00:00
{
$rules = [];
if ( $required ) {
$rules [] = 'required' ;
} else {
$rules [] = 'nullable' ;
}
$rules [] = 'string' ;
$rules [] = " min: $minLength " ;
$rules [] = " max: $maxLength " ;
2026-04-20 11:45:57 +00:00
if ( $enforcePattern ) {
$rules [] = 'regex:' . self :: DB_IDENTIFIER_PATTERN ;
}
2026-04-20 11:58:36 +00:00
return $rules ;
}
/**
* Get validation messages for database identifier fields .
*/
public static function databaseIdentifierMessages ( string $field , string $label = '' ) : array
{
$label = $label ? : $field ;
return [
" { $field } .regex " => " The { $label } may only contain letters, digits, and underscores, and must start with a letter or underscore. " ,
" { $field } .min " => " The { $label } must be at least :min character. " ,
" { $field } .max " => " The { $label } may not be greater than :max characters. " ,
];
}
/**
* Get validation rules for database password fields .
2026-04-20 11:45:57 +00:00
*
* Set $enforcePattern to false to skip the regex check ( for example when
* re - validating a legacy value on an existing record that has not been
* changed by the user ) . The length and type rules are always applied .
2026-04-20 11:58:36 +00:00
*/
2026-04-20 11:45:57 +00:00
public static function databasePasswordRules ( bool $required = true , int $minLength = 1 , int $maxLength = 128 , bool $enforcePattern = true ) : array
2026-04-20 11:58:36 +00:00
{
$rules = [];
if ( $required ) {
$rules [] = 'required' ;
} else {
$rules [] = 'nullable' ;
}
$rules [] = 'string' ;
$rules [] = " min: $minLength " ;
$rules [] = " max: $maxLength " ;
2026-04-20 11:45:57 +00:00
if ( $enforcePattern ) {
$rules [] = 'regex:' . self :: DB_PASSWORD_PATTERN ;
}
2026-04-20 11:58:36 +00:00
return $rules ;
}
/**
* Get validation messages for database password fields .
*/
public static function databasePasswordMessages ( string $field , string $label = '' ) : array
{
$label = $label ? : $field ;
return [
" { $field } .regex " => " The { $label } may not contain shell-unsafe characters (backtick, \$ , ;, |, &, <, >, \\ , quotes, spaces, or control characters). " ,
" { $field } .min " => " The { $label } must be at least :min character. " ,
" { $field } .max " => " The { $label } may not be greater than :max characters. " ,
];
}
/**
* Check if a string is a valid database identifier .
*/
public static function isValidDatabaseIdentifier ( string $value ) : bool
{
return preg_match ( self :: DB_IDENTIFIER_PATTERN , $value ) === 1 ;
}
2025-08-19 10:14:48 +00:00
/**
* Get validation rules for name fields
*/
public static function nameRules ( bool $required = true , int $minLength = 3 , int $maxLength = 255 ) : array
{
$rules = [];
if ( $required ) {
$rules [] = 'required' ;
} else {
$rules [] = 'nullable' ;
}
$rules [] = 'string' ;
$rules [] = " min: $minLength " ;
$rules [] = " max: $maxLength " ;
$rules [] = 'regex:' . self :: NAME_PATTERN ;
return $rules ;
}
/**
* Get validation rules for description fields
*/
public static function descriptionRules ( bool $required = false , int $maxLength = 255 ) : array
{
$rules = [];
if ( $required ) {
$rules [] = 'required' ;
} else {
$rules [] = 'nullable' ;
}
$rules [] = 'string' ;
$rules [] = " max: $maxLength " ;
$rules [] = 'regex:' . self :: DESCRIPTION_PATTERN ;
return $rules ;
}
/**
* Get validation messages for name fields
*/
public static function nameMessages () : array
{
return [
2026-03-24 07:03:08 +00:00
'name.regex' => 'The name may only contain letters (including Unicode), numbers, spaces, and these characters: - _ . / @ & ( ) # , : +' ,
2025-08-19 10:14:48 +00:00
'name.min' => 'The name must be at least :min characters.' ,
'name.max' => 'The name may not be greater than :max characters.' ,
];
}
/**
* Get validation messages for description fields
*/
public static function descriptionMessages () : array
{
return [
2026-01-19 17:50:56 +00:00
'description.regex' => " The description may only contain letters (including Unicode), numbers, spaces, and common punctuation: - _ . , ! ? ( ) ' \" + = * / @ & " ,
2025-08-19 10:14:48 +00:00
'description.max' => 'The description may not be greater than :max characters.' ,
];
}
2026-03-12 12:09:13 +00:00
/**
* Get validation rules for file path fields ( dockerfile location , docker compose location )
*/
public static function filePathRules ( int $maxLength = 255 ) : array
{
return [ 'nullable' , 'string' , 'max:' . $maxLength , 'regex:' . self :: FILE_PATH_PATTERN ];
}
/**
* Get validation messages for file path fields
*/
public static function filePathMessages ( string $field = 'dockerfileLocation' , string $label = 'Dockerfile' ) : array
{
return [
" { $field } .regex " => " The { $label } location must be a valid path starting with / and containing only alphanumeric characters, dots, hyphens, underscores, slashes, @, ~, and +. " ,
];
}
2026-03-18 12:53:01 +00:00
/**
* Get validation rules for directory path fields ( base_directory , publish_directory )
*/
public static function directoryPathRules ( int $maxLength = 255 ) : array
{
return [ 'nullable' , 'string' , 'max:' . $maxLength , 'regex:' . self :: DIRECTORY_PATH_PATTERN ];
}
/**
* Get validation rules for Docker build target fields
*/
public static function dockerTargetRules ( int $maxLength = 128 ) : array
{
return [ 'nullable' , 'string' , 'max:' . $maxLength , 'regex:' . self :: DOCKER_TARGET_PATTERN ];
}
/**
* Get validation rules for shell - safe command fields
*/
public static function shellSafeCommandRules ( int $maxLength = 1000 ) : array
{
return [ 'nullable' , 'string' , 'max:' . $maxLength , 'regex:' . self :: SHELL_SAFE_COMMAND_PATTERN ];
}
2026-03-26 10:06:30 +00:00
/**
* Get validation rules for Docker volume name fields
*/
public static function volumeNameRules ( bool $required = true , int $maxLength = 255 ) : array
{
$rules = [];
if ( $required ) {
$rules [] = 'required' ;
} else {
$rules [] = 'nullable' ;
}
$rules [] = 'string' ;
$rules [] = " max: $maxLength " ;
$rules [] = 'regex:' . self :: VOLUME_NAME_PATTERN ;
return $rules ;
}
/**
* Get validation messages for volume name fields
*/
public static function volumeNameMessages ( string $field = 'name' ) : array
{
return [
" { $field } .regex " => 'The volume name must start with an alphanumeric character and contain only alphanumeric characters, dots, hyphens, and underscores.' ,
];
}
2026-03-28 17:53:25 +00:00
/**
2026-04-11 16:54:52 +00:00
* Pattern for port mappings with optional IP binding and protocol suffix on either side .
* Format : [ ip : ] port [ : ip : port ] where IP is IPv4 or [ IPv6 ], port can be a range , protocol suffix optional .
* Examples : 8080 : 80 , 127.0 . 0.1 : 8080 : 80 , [ :: 1 ] :: 80 / udp , 127.0 . 0.1 : 8080 : 80 / tcp
2026-03-28 17:53:25 +00:00
*/
2026-04-11 16:54:52 +00:00
public const PORT_MAPPINGS_PATTERN = ' /^
( ? : ( ? : \d { 1 , 3 } \ . \d { 1 , 3 } \ . \d { 1 , 3 } \ . \d { 1 , 3 } | \ [[ \da - fA - F : ] + \ ]) : ) ? # optional IP
( ? : \d + ( ? :- \d + ) ? ( ? : \ / ( ? : tcp | udp | sctp )) ? ) ? # optional host port
:
( ? : ( ? : \d { 1 , 3 } \ . \d { 1 , 3 } \ . \d { 1 , 3 } \ . \d { 1 , 3 } | \ [[ \da - fA - F : ] + \ ]) : ) ? # optional IP
\d + ( ? :- \d + ) ? ( ? : \ / ( ? : tcp | udp | sctp )) ? # container port
( ? : ,
( ? : ( ? : \d { 1 , 3 } \ . \d { 1 , 3 } \ . \d { 1 , 3 } \ . \d { 1 , 3 } | \ [[ \da - fA - F : ] + \ ]) : ) ?
( ? : \d + ( ? :- \d + ) ? ( ? : \ / ( ? : tcp | udp | sctp )) ? ) ?
:
( ? : ( ? : \d { 1 , 3 } \ . \d { 1 , 3 } \ . \d { 1 , 3 } \ . \d { 1 , 3 } | \ [[ \da - fA - F : ] + \ ]) : ) ?
\d + ( ? :- \d + ) ? ( ? : \ / ( ? : tcp | udp | sctp )) ?
) *
$ / x ' ;
2026-03-28 17:53:25 +00:00
2026-03-18 12:53:01 +00:00
/**
* Get validation rules for container name fields
*/
public static function containerNameRules ( int $maxLength = 255 ) : array
{
return [ 'string' , 'max:' . $maxLength , 'regex:' . self :: CONTAINER_NAME_PATTERN ];
}
2026-03-28 17:53:25 +00:00
/**
* Get validation rules for port mapping fields
*/
public static function portMappingRules () : array
{
return [ 'nullable' , 'string' , 'regex:' . self :: PORT_MAPPINGS_PATTERN ];
}
/**
* Get validation messages for port mapping fields
*/
public static function portMappingMessages ( string $field = 'portsMappings' ) : array
{
return [
2026-04-11 16:54:52 +00:00
" { $field } .regex " => 'Port mappings must be a comma-separated list of port pairs or ranges with optional IP and protocol (e.g. 3000:3000, 8080:80/udp, 127.0.0.1:8080:80, [::1]::80).' ,
2026-03-28 17:53:25 +00:00
];
}
2026-03-25 19:21:39 +00:00
/**
* Check if a string is a valid Docker container name .
*/
public static function isValidContainerName ( string $name ) : bool
{
return preg_match ( self :: CONTAINER_NAME_PATTERN , $name ) === 1 ;
}
2026-03-28 11:28:59 +00:00
/**
* Get validation rules for Docker network name fields
*/
public static function dockerNetworkRules ( bool $required = true , int $maxLength = 255 ) : array
{
$rules = [];
if ( $required ) {
$rules [] = 'required' ;
} else {
$rules [] = 'nullable' ;
}
$rules [] = 'string' ;
$rules [] = " max: $maxLength " ;
$rules [] = 'regex:' . self :: DOCKER_NETWORK_PATTERN ;
return $rules ;
}
/**
* Get validation messages for Docker network name fields
*/
public static function dockerNetworkMessages ( string $field = 'network' ) : array
{
return [
" { $field } .regex " => 'The network name must start with an alphanumeric character and contain only alphanumeric characters, dots, hyphens, and underscores.' ,
];
}
/**
* Check if a string is a valid Docker network name .
*/
public static function isValidDockerNetwork ( string $name ) : bool
{
return preg_match ( self :: DOCKER_NETWORK_PATTERN , $name ) === 1 ;
}
2026-03-12 12:09:13 +00:00
/**
2025-08-19 10:14:48 +00:00
* Get combined validation messages for both name and description fields
*/
public static function combinedMessages () : array
{
return array_merge ( self :: nameMessages (), self :: descriptionMessages ());
}
}