- Add .distignore (operator-private files excluded) - Add build.sh for WordPress-installable release ZIPs - Update CLAUDE.md references (now operator-private only)
379 lines
14 KiB
PHP
379 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* TigerStyle Scent Input Validation Framework
|
|
* Comprehensive validation system that sets the gold standard for WordPress security
|
|
*
|
|
* @package TigerStyle Scent
|
|
*/
|
|
|
|
defined('ABSPATH') or die('Direct access forbidden.');
|
|
|
|
class TigerStyleScent_InputValidator {
|
|
|
|
/**
|
|
* 🔐 SECURITY: Comprehensive OAuth2 parameter validation
|
|
*
|
|
* @param array $data Input data to validate
|
|
* @param array $rules Validation rules
|
|
* @return array Validation results with sanitized data
|
|
*/
|
|
public static function validate_oauth2_request(array $data, array $rules): array {
|
|
$result = [
|
|
'valid' => true,
|
|
'errors' => [],
|
|
'sanitized' => [],
|
|
'warnings' => []
|
|
];
|
|
|
|
foreach ($rules as $field => $rule_set) {
|
|
$value = $data[$field] ?? null;
|
|
$field_result = self::validate_field($field, $value, $rule_set);
|
|
|
|
if (!$field_result['valid']) {
|
|
$result['valid'] = false;
|
|
$result['errors'][$field] = $field_result['errors'];
|
|
}
|
|
|
|
if (!empty($field_result['warnings'])) {
|
|
$result['warnings'][$field] = $field_result['warnings'];
|
|
}
|
|
|
|
$result['sanitized'][$field] = $field_result['sanitized'];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Validate individual field with comprehensive rules
|
|
*/
|
|
private static function validate_field(string $field, $value, array $rules): array {
|
|
$result = [
|
|
'valid' => true,
|
|
'errors' => [],
|
|
'warnings' => [],
|
|
'sanitized' => $value
|
|
];
|
|
|
|
// Required field check
|
|
if (isset($rules['required']) && $rules['required'] && empty($value)) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = "Field '{$field}' is required";
|
|
return $result;
|
|
}
|
|
|
|
// Skip further validation if field is empty and not required
|
|
if (empty($value) && !($rules['required'] ?? false)) {
|
|
return $result;
|
|
}
|
|
|
|
// Apply validation rules
|
|
foreach ($rules as $rule => $rule_value) {
|
|
switch ($rule) {
|
|
case 'type':
|
|
$type_result = self::validate_type($value, $rule_value);
|
|
if (!$type_result['valid']) {
|
|
$result['valid'] = false;
|
|
$result['errors'] = array_merge($result['errors'], $type_result['errors']);
|
|
}
|
|
$result['sanitized'] = $type_result['sanitized'];
|
|
break;
|
|
|
|
case 'length':
|
|
$length_result = self::validate_length($value, $rule_value);
|
|
if (!$length_result['valid']) {
|
|
$result['valid'] = false;
|
|
$result['errors'] = array_merge($result['errors'], $length_result['errors']);
|
|
}
|
|
break;
|
|
|
|
case 'pattern':
|
|
$pattern_result = self::validate_pattern($value, $rule_value);
|
|
if (!$pattern_result['valid']) {
|
|
$result['valid'] = false;
|
|
$result['errors'] = array_merge($result['errors'], $pattern_result['errors']);
|
|
}
|
|
break;
|
|
|
|
case 'enum':
|
|
$enum_result = self::validate_enum($value, $rule_value);
|
|
if (!$enum_result['valid']) {
|
|
$result['valid'] = false;
|
|
$result['errors'] = array_merge($result['errors'], $enum_result['errors']);
|
|
}
|
|
break;
|
|
|
|
case 'security':
|
|
$security_result = self::validate_security($value, $rule_value);
|
|
if (!$security_result['valid']) {
|
|
$result['valid'] = false;
|
|
$result['errors'] = array_merge($result['errors'], $security_result['errors']);
|
|
}
|
|
if (!empty($security_result['warnings'])) {
|
|
$result['warnings'] = array_merge($result['warnings'], $security_result['warnings']);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Type validation with WordPress sanitization
|
|
*/
|
|
private static function validate_type($value, string $type): array {
|
|
$result = ['valid' => true, 'errors' => [], 'sanitized' => $value];
|
|
|
|
switch ($type) {
|
|
case 'oauth2_client_id':
|
|
// OAuth2 client IDs should be alphanumeric with limited special chars
|
|
$sanitized = preg_replace('/[^a-zA-Z0-9._-]/', '', $value);
|
|
if ($sanitized !== $value) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Client ID contains invalid characters';
|
|
}
|
|
$result['sanitized'] = $sanitized;
|
|
break;
|
|
|
|
case 'oauth2_scope':
|
|
// OAuth2 scopes: space-separated tokens with limited chars
|
|
$sanitized = preg_replace('/[^a-zA-Z0-9:._\s-]/', '', $value);
|
|
$result['sanitized'] = trim($sanitized);
|
|
if ($sanitized !== $value) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Scope contains invalid characters';
|
|
}
|
|
break;
|
|
|
|
case 'oauth2_response_type':
|
|
$result['sanitized'] = sanitize_text_field($value);
|
|
break;
|
|
|
|
case 'oauth2_grant_type':
|
|
$result['sanitized'] = sanitize_text_field($value);
|
|
break;
|
|
|
|
case 'url':
|
|
$sanitized = esc_url_raw($value);
|
|
if (empty($sanitized) || $sanitized !== $value) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Invalid URL format';
|
|
}
|
|
$result['sanitized'] = $sanitized;
|
|
break;
|
|
|
|
case 'oauth2_code':
|
|
// Authorization codes should be base64url-like
|
|
$sanitized = preg_replace('/[^a-zA-Z0-9._-]/', '', $value);
|
|
if ($sanitized !== $value) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Authorization code contains invalid characters';
|
|
}
|
|
$result['sanitized'] = $sanitized;
|
|
break;
|
|
|
|
case 'oauth2_token':
|
|
// Access tokens should be base64url-like
|
|
$sanitized = preg_replace('/[^a-zA-Z0-9._-]/', '', $value);
|
|
if ($sanitized !== $value) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Token contains invalid characters';
|
|
}
|
|
$result['sanitized'] = $sanitized;
|
|
break;
|
|
|
|
case 'text':
|
|
$result['sanitized'] = sanitize_text_field($value);
|
|
break;
|
|
|
|
default:
|
|
$result['sanitized'] = sanitize_text_field($value);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Length validation
|
|
*/
|
|
private static function validate_length($value, array $constraints): array {
|
|
$result = ['valid' => true, 'errors' => []];
|
|
$length = strlen($value);
|
|
|
|
if (isset($constraints['min']) && $length < $constraints['min']) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = "Value too short (minimum {$constraints['min']} characters)";
|
|
}
|
|
|
|
if (isset($constraints['max']) && $length > $constraints['max']) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = "Value too long (maximum {$constraints['max']} characters)";
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Pattern validation with security considerations
|
|
*/
|
|
private static function validate_pattern($value, string $pattern): array {
|
|
$result = ['valid' => true, 'errors' => []];
|
|
|
|
if (!preg_match($pattern, $value)) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Value does not match required pattern';
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Enumeration validation
|
|
*/
|
|
private static function validate_enum($value, array $allowed_values): array {
|
|
$result = ['valid' => true, 'errors' => []];
|
|
|
|
if (!in_array($value, $allowed_values, true)) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Value not in allowed list: ' . implode(', ', $allowed_values);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Advanced security validation
|
|
*/
|
|
private static function validate_security($value, array $security_rules): array {
|
|
$result = ['valid' => true, 'errors' => [], 'warnings' => []];
|
|
|
|
// Check for SQL injection patterns
|
|
if (isset($security_rules['sql_injection']) && $security_rules['sql_injection']) {
|
|
$sql_patterns = [
|
|
'/(\b(select|insert|update|delete|drop|create|alter|exec|execute)\b)/i',
|
|
'/(\b(union|or|and)\s+[\w\s]*=)/i',
|
|
'/(\'|\"|;|--|\*|\/\*|\*\/)/i'
|
|
];
|
|
|
|
foreach ($sql_patterns as $pattern) {
|
|
if (preg_match($pattern, $value)) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Potential SQL injection attempt detected';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for XSS patterns
|
|
if (isset($security_rules['xss']) && $security_rules['xss']) {
|
|
$xss_patterns = [
|
|
'/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/i',
|
|
'/javascript:/i',
|
|
'/on\w+\s*=/i',
|
|
'/<iframe\b/i',
|
|
'/<object\b/i',
|
|
'/<embed\b/i'
|
|
];
|
|
|
|
foreach ($xss_patterns as $pattern) {
|
|
if (preg_match($pattern, $value)) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Potential XSS attempt detected';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for suspicious characters
|
|
if (isset($security_rules['suspicious_chars']) && $security_rules['suspicious_chars']) {
|
|
if (preg_match('/[<>"\'\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value)) {
|
|
$result['warnings'][] = 'Contains potentially suspicious characters';
|
|
}
|
|
}
|
|
|
|
// Check for common attack patterns
|
|
if (isset($security_rules['attack_patterns']) && $security_rules['attack_patterns']) {
|
|
$attack_patterns = [
|
|
'/\.\.[\/\\\\]/', // Directory traversal
|
|
'/\x00/', // Null bytes
|
|
'/eval\s*\(/i', // Code injection
|
|
'/system\s*\(/i', // Command injection
|
|
];
|
|
|
|
foreach ($attack_patterns as $pattern) {
|
|
if (preg_match($pattern, $value)) {
|
|
$result['valid'] = false;
|
|
$result['errors'][] = 'Security threat pattern detected';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 🔐 SECURITY: Get OAuth2 validation rules
|
|
*/
|
|
public static function get_oauth2_validation_rules(): array {
|
|
return [
|
|
'client_id' => [
|
|
'required' => true,
|
|
'type' => 'oauth2_client_id',
|
|
'length' => ['min' => 1, 'max' => 255],
|
|
'pattern' => '/^[a-zA-Z0-9._-]+$/',
|
|
'security' => ['sql_injection' => true, 'xss' => true, 'attack_patterns' => true]
|
|
],
|
|
'client_secret' => [
|
|
'required' => false, // May not be required for public clients
|
|
'type' => 'text',
|
|
'length' => ['min' => 1, 'max' => 1000],
|
|
'security' => ['sql_injection' => true, 'xss' => true, 'attack_patterns' => true]
|
|
],
|
|
'response_type' => [
|
|
'required' => true,
|
|
'type' => 'oauth2_response_type',
|
|
'enum' => ['code', 'token', 'id_token']
|
|
],
|
|
'grant_type' => [
|
|
'required' => true,
|
|
'type' => 'oauth2_grant_type',
|
|
'enum' => ['authorization_code', 'refresh_token', 'client_credentials', 'password']
|
|
],
|
|
'redirect_uri' => [
|
|
'required' => true,
|
|
'type' => 'url',
|
|
'length' => ['max' => 2048],
|
|
'security' => ['xss' => true, 'attack_patterns' => true]
|
|
],
|
|
'scope' => [
|
|
'required' => false,
|
|
'type' => 'oauth2_scope',
|
|
'length' => ['max' => 1000],
|
|
'pattern' => '/^[a-zA-Z0-9:._\s-]*$/',
|
|
'security' => ['sql_injection' => true, 'xss' => true]
|
|
],
|
|
'state' => [
|
|
'required' => false,
|
|
'type' => 'text',
|
|
'length' => ['max' => 1000],
|
|
'security' => ['sql_injection' => true, 'xss' => true, 'suspicious_chars' => true]
|
|
],
|
|
'code' => [
|
|
'required' => false,
|
|
'type' => 'oauth2_code',
|
|
'length' => ['min' => 10, 'max' => 512],
|
|
'pattern' => '/^[a-zA-Z0-9._-]+$/',
|
|
'security' => ['sql_injection' => true, 'attack_patterns' => true]
|
|
],
|
|
'token' => [
|
|
'required' => false,
|
|
'type' => 'oauth2_token',
|
|
'length' => ['min' => 10, 'max' => 1024],
|
|
'pattern' => '/^[a-zA-Z0-9._-]+$/',
|
|
'security' => ['sql_injection' => true, 'attack_patterns' => true]
|
|
]
|
|
];
|
|
}
|
|
} |