coolify/app/Rules/ValidHostname.php
Andras Bacsai 988dd57cf4 feat(validation): make hostname validation case-insensitive and expand allowed characters
- Normalize hostnames to lowercase for RFC 1123 compliance while accepting uppercase input
- Expand NAME_PATTERN to allow parentheses, hash, comma, colon, and plus characters
- Add fallback to random name generation when application name doesn't meet minimum requirements
- Add comprehensive test coverage for validation patterns and edge cases
2026-03-24 08:03:08 +01:00

117 lines
4 KiB
PHP

<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Log;
class ValidHostname implements ValidationRule
{
/**
* Run the validation rule.
*
* Validates hostname according to RFC 1123:
* - Must be 1-253 characters total
* - Each label (segment between dots) must be 1-63 characters
* - Labels can contain lowercase letters (a-z), digits (0-9), and hyphens (-)
* - Labels cannot start or end with a hyphen
* - Labels cannot be all numeric
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (empty($value)) {
return;
}
$hostname = trim($value);
// Check total length (RFC 1123: max 253 characters)
if (strlen($hostname) > 253) {
$fail('The :attribute must not exceed 253 characters.');
return;
}
// Check for dangerous shell metacharacters
$dangerousChars = [
';', '|', '&', '$', '`', '(', ')', '{', '}',
'<', '>', '\n', '\r', '\0', '"', "'", '\\',
'!', '*', '?', '[', ']', '~', '^', ':', '#',
'@', '%', '=', '+', ',', ' ',
];
foreach ($dangerousChars as $char) {
if (str_contains($hostname, $char)) {
try {
$logData = [
'hostname' => $hostname,
'character' => $char,
];
if (function_exists('request') && app()->has('request')) {
$logData['ip'] = request()->ip();
}
if (function_exists('auth') && app()->has('auth')) {
$logData['user_id'] = auth()->id();
}
Log::warning('Hostname validation failed - dangerous character', $logData);
} catch (\Throwable $e) {
// Ignore errors when facades are not available (e.g., in unit tests)
}
$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.');
return;
}
// Check for consecutive dots
if (str_contains($hostname, '..')) {
$fail('The :attribute cannot contain consecutive dots.');
return;
}
// Split into labels (segments between dots)
$labels = explode('.', $hostname);
foreach ($labels as $label) {
// Check label length (RFC 1123: max 63 characters per label)
if (strlen($label) < 1 || strlen($label) > 63) {
$fail('The :attribute contains an invalid label. Each segment must be 1-63 characters.');
return;
}
// Check if label starts or ends with hyphen
if (str_starts_with($label, '-') || str_ends_with($label, '-')) {
$fail('The :attribute contains an invalid label. Labels cannot start or end with a hyphen.');
return;
}
// Check if label contains only valid characters (letters, digits, hyphens)
if (! preg_match('/^[a-z0-9-]+$/', $label)) {
$fail('The :attribute contains invalid characters. Only letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
return;
}
// RFC 1123 allows labels to be all numeric (unlike RFC 952)
// So we don't need to check for all-numeric labels
}
}
}