Add CSRF, CAPTCHA, and input validation improvements

Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
This commit is contained in:
Hosteroid
2025-10-10 00:04:12 +03:00
parent 98f37c2482
commit a29becc944
41 changed files with 1689 additions and 77 deletions

View File

@@ -0,0 +1,207 @@
<?php
namespace App\Helpers;
/**
* Input Validator
*
* Centralized input validation helper for consistent validation across the application
*/
class InputValidator
{
/**
* Validate domain name format
*
* @param string $domain Domain name to validate
* @return bool True if valid, false otherwise
*/
public static function validateDomain(string $domain): bool
{
// Check length (max 253 characters per RFC 1035)
if (strlen($domain) > 253 || strlen($domain) < 3) {
return false;
}
// Validate domain format
// Allows: example.com, sub.example.com, example.co.uk
// Pattern: alphanumeric with hyphens, dots between labels, valid TLD
return (bool)preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $domain);
}
/**
* Validate text field length
*
* @param string $value Value to validate
* @param int $max Maximum length
* @param string $fieldName Field name for error message
* @return string|null Error message or null if valid
*/
public static function validateLength(string $value, int $max, string $fieldName = 'Field'): ?string
{
if (strlen($value) > $max) {
return "$fieldName is too long (maximum $max characters)";
}
return null;
}
/**
* Validate string is within min and max length
*
* @param string $value Value to validate
* @param int $min Minimum length
* @param int $max Maximum length
* @param string $fieldName Field name for error message
* @return string|null Error message or null if valid
*/
public static function validateLengthRange(string $value, int $min, int $max, string $fieldName = 'Field'): ?string
{
$len = strlen($value);
if ($len < $min) {
return "$fieldName is too short (minimum $min characters)";
}
if ($len > $max) {
return "$fieldName is too long (maximum $max characters)";
}
return null;
}
/**
* Sanitize text input (remove control characters)
*
* @param string $text Text to sanitize
* @return string Sanitized text
*/
public static function sanitizeText(string $text): string
{
// Remove control characters (except tabs and newlines for text areas)
$text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text);
return trim($text);
}
/**
* Validate array size is within limits
*
* @param array $array Array to validate
* @param int $max Maximum number of elements
* @param string $fieldName Field name for error message
* @return string|null Error message or null if valid
*/
public static function validateArraySize(array $array, int $max, string $fieldName = 'Selection'): ?string
{
if (count($array) > $max) {
return "$fieldName exceeds maximum of $max items";
}
return null;
}
/**
* Sanitize search query
*
* @param string $query Search query
* @param int $maxLength Maximum length (default 100)
* @return string Sanitized query
*/
public static function sanitizeSearch(string $query, int $maxLength = 100): string
{
// Remove control characters
$query = self::sanitizeText($query);
// Limit length
if (strlen($query) > $maxLength) {
$query = substr($query, 0, $maxLength);
}
return $query;
}
/**
* Validate username format
*
* @param string $username Username to validate
* @param int $minLength Minimum length (default 3)
* @param int $maxLength Maximum length (default 50)
* @return string|null Error message or null if valid
*/
public static function validateUsername(string $username, int $minLength = 3, int $maxLength = 50): ?string
{
if (strlen($username) < $minLength) {
return "Username must be at least $minLength characters";
}
if (strlen($username) > $maxLength) {
return "Username must not exceed $maxLength characters";
}
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
return 'Username can only contain letters, numbers, and underscores';
}
return null;
}
/**
* Validate URL format
*
* @param string $url URL to validate
* @return bool True if valid, false otherwise
*/
public static function validateUrl(string $url): bool
{
return filter_var($url, FILTER_VALIDATE_URL) !== false;
}
/**
* Validate email format
*
* @param string $email Email to validate
* @return bool True if valid, false otherwise
*/
public static function validateEmail(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Validate numeric value is within range
*
* @param mixed $value Value to validate
* @param int|float $min Minimum value
* @param int|float $max Maximum value
* @param string $fieldName Field name for error message
* @return string|null Error message or null if valid
*/
public static function validateRange($value, $min, $max, string $fieldName = 'Value'): ?string
{
if (!is_numeric($value)) {
return "$fieldName must be a number";
}
$numValue = is_float($min) || is_float($max) ? floatval($value) : intval($value);
if ($numValue < $min || $numValue > $max) {
return "$fieldName must be between $min and $max";
}
return null;
}
/**
* Validate value is in allowed list (whitelist)
*
* @param mixed $value Value to validate
* @param array $allowed Allowed values
* @param string $fieldName Field name for error message
* @return string|null Error message or null if valid
*/
public static function validateInList($value, array $allowed, string $fieldName = 'Value'): ?string
{
if (!in_array($value, $allowed, true)) {
return "$fieldName has an invalid value";
}
return null;
}
}