- Implemented SecurityTest to validate token generation, CSRF protection, input sanitization, and rate limiting. - Created UserModelTest to ensure correct database operations for user management, including creation, updating, banning, and fetching active users. - Developed ValidationTest to verify input validation and sanitization for user IDs, nicknames, messages, and API requests. - Introduced Security and Validation utility classes with methods for secure token generation, input sanitization, and comprehensive validation rules.
316 lines
11 KiB
PHP
316 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Input Validation Utilities
|
|
* Comprehensive input validation and sanitization functions
|
|
*/
|
|
|
|
class Validation
|
|
{
|
|
/**
|
|
* Validate and sanitize nickname
|
|
*/
|
|
public static function validateNickname($nickname)
|
|
{
|
|
if (!is_string($nickname)) {
|
|
return self::createValidationResult(false, 'Nickname must be a string');
|
|
}
|
|
|
|
$trimmed = trim($nickname);
|
|
|
|
// Check length
|
|
if (strlen($trimmed) < 1) {
|
|
return self::createValidationResult(false, 'Nickname cannot be empty');
|
|
}
|
|
|
|
if (strlen($trimmed) > 20) {
|
|
return self::createValidationResult(false, 'Nickname must be 20 characters or less');
|
|
}
|
|
|
|
// Check allowed characters (alphanumeric, spaces, hyphens, apostrophes)
|
|
if (!preg_match('/^[a-zA-Z0-9\s\'-]+$/', $trimmed)) {
|
|
return self::createValidationResult(false, 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes');
|
|
}
|
|
|
|
// Sanitize
|
|
$sanitized = Security::sanitizeInput($trimmed);
|
|
|
|
return self::createValidationResult(true, 'Valid nickname', $sanitized);
|
|
}
|
|
|
|
/**
|
|
* Validate and sanitize chat message
|
|
*/
|
|
public static function validateMessage($message, $maxLength = 1000)
|
|
{
|
|
if (!is_string($message)) {
|
|
return self::createValidationResult(false, 'Message must be a string');
|
|
}
|
|
|
|
$trimmed = trim($message);
|
|
|
|
// Check length
|
|
if (strlen($trimmed) < 1) {
|
|
return self::createValidationResult(false, 'Message cannot be empty');
|
|
}
|
|
|
|
if (strlen($trimmed) > $maxLength) {
|
|
return self::createValidationResult(false, 'Message exceeds maximum length of ' . $maxLength . ' characters');
|
|
}
|
|
|
|
// Basic XSS check (additional to sanitization)
|
|
$xssPatterns = [
|
|
'/<script[^>]*>.*?<\/script>/is',
|
|
'/javascript:/i',
|
|
'/on\w+\s*=/i',
|
|
'/<iframe[^>]*>.*?<\/iframe>/is'
|
|
];
|
|
|
|
foreach ($xssPatterns as $pattern) {
|
|
if (preg_match($pattern, $trimmed)) {
|
|
return self::createValidationResult(false, 'Message contains potentially harmful content');
|
|
}
|
|
}
|
|
|
|
// Sanitize
|
|
$sanitized = Security::sanitizeInput($trimmed);
|
|
|
|
return self::createValidationResult(true, 'Valid message', $sanitized);
|
|
}
|
|
|
|
/**
|
|
* Validate user ID format
|
|
*/
|
|
public static function validateUserId($userId)
|
|
{
|
|
if (!is_string($userId)) {
|
|
return self::createValidationResult(false, 'User ID must be a string');
|
|
}
|
|
|
|
// Should be hexadecimal (from bin2hex)
|
|
if (!preg_match('/^[a-f0-9]+$/', $userId)) {
|
|
return self::createValidationResult(false, 'Invalid user ID format');
|
|
}
|
|
|
|
// Check length (16 characters for 8 bytes)
|
|
if (strlen($userId) !== 16) {
|
|
return self::createValidationResult(false, 'Invalid user ID length');
|
|
}
|
|
|
|
return self::createValidationResult(true, 'Valid user ID', $userId);
|
|
}
|
|
|
|
/**
|
|
* Validate admin login credentials
|
|
*/
|
|
public static function validateAdminLogin($username, $password)
|
|
{
|
|
// Username validation
|
|
if (empty($username) || !is_string($username)) {
|
|
return self::createValidationResult(false, 'Username is required');
|
|
}
|
|
|
|
$username = trim($username);
|
|
if (strlen($username) < 3 || strlen($username) > 50) {
|
|
return self::createValidationResult(false, 'Username must be between 3-50 characters');
|
|
}
|
|
|
|
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $username)) {
|
|
return self::createValidationResult(false, 'Username can only contain letters, numbers, hyphens, and underscores');
|
|
}
|
|
|
|
// Password validation
|
|
if (empty($password) || !is_string($password)) {
|
|
return self::createValidationResult(false, 'Password is required');
|
|
}
|
|
|
|
$password = trim($password);
|
|
if (strlen($password) < 6) {
|
|
return self::createValidationResult(false, 'Password must be at least 6 characters long');
|
|
}
|
|
|
|
// Additional password strength checks (optional)
|
|
$hasUppercase = preg_match('/[A-Z]/', $password);
|
|
$hasLowercase = preg_match('/[a-z]/', $password);
|
|
$hasNumbers = preg_match('/[0-9]/', $password);
|
|
|
|
if (!$hasUppercase || !$hasLowercase || !$hasNumbers) {
|
|
return self::createValidationResult(false, 'Password must contain at least one uppercase letter, one lowercase letter, and one number');
|
|
}
|
|
|
|
return self::createValidationResult(true, 'Credentials format valid', [
|
|
'username' => $username,
|
|
'password' => $password
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Validate API request parameters
|
|
*/
|
|
public static function validateApiRequest($params, $rules)
|
|
{
|
|
$validated = [];
|
|
$errors = [];
|
|
|
|
foreach ($rules as $field => $rule) {
|
|
$value = $params[$field] ?? null;
|
|
|
|
// Check required fields
|
|
if (isset($rule['required']) && $rule['required'] && $value === null) {
|
|
$errors[$field] = $rule['required_message'] ?? "{$field} is required";
|
|
continue;
|
|
}
|
|
|
|
// Skip validation if not required and not provided
|
|
if ($value === null) {
|
|
continue;
|
|
}
|
|
|
|
// Type validation
|
|
if (isset($rule['type'])) {
|
|
$valid = self::validateType($value, $rule['type'], $rule);
|
|
if (!$valid['valid']) {
|
|
$errors[$field] = $valid['message'];
|
|
continue;
|
|
}
|
|
$validated[$field] = $valid['value'];
|
|
continue;
|
|
}
|
|
|
|
// Apply sanitization
|
|
if (isset($rule['sanitize'])) {
|
|
$value = Security::sanitizeInput($value, $rule['sanitize']);
|
|
}
|
|
|
|
$validated[$field] = $value;
|
|
}
|
|
|
|
return [
|
|
'valid' => empty($errors),
|
|
'validated' => $validated,
|
|
'errors' => $errors
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Validate value against specific type
|
|
*/
|
|
private static function validateType($value, $type, $rule = [])
|
|
{
|
|
switch ($type) {
|
|
case 'string':
|
|
if (!is_string($value)) {
|
|
return ['valid' => false, 'message' => 'Must be a string'];
|
|
}
|
|
$value = trim($value);
|
|
|
|
if (isset($rule['min_length']) && strlen($value) < $rule['min_length']) {
|
|
return ['valid' => false, 'message' => "Must be at least {$rule['min_length']} characters long"];
|
|
}
|
|
|
|
if (isset($rule['max_length']) && strlen($value) > $rule['max_length']) {
|
|
return ['valid' => false, 'message' => "Must be no more than {$rule['max_length']} characters long"];
|
|
}
|
|
|
|
if (isset($rule['pattern']) && !preg_match($rule['pattern'], $value)) {
|
|
return ['valid' => false, 'message' => $rule['pattern_message'] ?? 'Invalid format'];
|
|
}
|
|
|
|
return ['valid' => true, 'value' => $value];
|
|
|
|
case 'int':
|
|
$intVal = filter_var($value, FILTER_VALIDATE_INT);
|
|
if ($intVal === false) {
|
|
return ['valid' => false, 'message' => 'Must be a valid integer'];
|
|
}
|
|
|
|
if (isset($rule['min']) && $intVal < $rule['min']) {
|
|
return ['valid' => false, 'message' => "Must be at least {$rule['min']}"];
|
|
}
|
|
|
|
if (isset($rule['max']) && $intVal > $rule['max']) {
|
|
return ['valid' => false, 'message' => "Must be no more than {$rule['max']}"];
|
|
}
|
|
|
|
return ['valid' => true, 'value' => $intVal];
|
|
|
|
case 'email':
|
|
$email = filter_var(trim($value), FILTER_VALIDATE_EMAIL);
|
|
if (!$email) {
|
|
return ['valid' => false, 'message' => 'Invalid email address'];
|
|
}
|
|
return ['valid' => true, 'value' => $email];
|
|
|
|
case 'boolean':
|
|
$boolVal = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($boolVal === null) {
|
|
return ['valid' => false, 'message' => 'Must be a boolean value'];
|
|
}
|
|
return ['valid' => true, 'value' => $boolVal];
|
|
|
|
default:
|
|
return ['valid' => true, 'value' => $value];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate heartbeat data
|
|
*/
|
|
public static function validateHeartbeat($data)
|
|
{
|
|
$rules = [
|
|
'nickname' => [
|
|
'required' => false,
|
|
'type' => 'string',
|
|
'max_length' => 20,
|
|
'pattern' => '/^[a-zA-Z0-9\s\'-]*$/',
|
|
'pattern_message' => 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes'
|
|
]
|
|
];
|
|
|
|
return self::validateApiRequest($data, $rules);
|
|
}
|
|
|
|
/**
|
|
* Validate message send request
|
|
*/
|
|
public static function validateMessageSend($data)
|
|
{
|
|
$rules = [
|
|
'nickname' => [
|
|
'required' => true,
|
|
'type' => 'string',
|
|
'min_length' => 1,
|
|
'max_length' => 20,
|
|
'pattern' => '/^[a-zA-Z0-9\s\'-]+$/',
|
|
'pattern_message' => 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes',
|
|
'required_message' => 'Nickname is required'
|
|
],
|
|
'message' => [
|
|
'required' => true,
|
|
'type' => 'string',
|
|
'min_length' => 1,
|
|
'max_length' => 1000,
|
|
'required_message' => 'Message cannot be empty'
|
|
]
|
|
];
|
|
|
|
return self::validateApiRequest($data, $rules);
|
|
}
|
|
|
|
/**
|
|
* Create validation result array
|
|
*/
|
|
private static function createValidationResult($valid, $message, $data = null)
|
|
{
|
|
$result = [
|
|
'valid' => $valid,
|
|
'message' => $message
|
|
];
|
|
|
|
if ($data !== null) {
|
|
$result['data'] = $data;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|