62 lines
1.8 KiB
PHP
62 lines
1.8 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Sanitize user-entered free text before storage (defense in depth; queries use prepared statements).
|
|
*/
|
|
function sanitize_free_text(mixed $value, int $maxLength = 2000): string {
|
|
if (!is_string($value) && !is_numeric($value)) {
|
|
return '';
|
|
}
|
|
$s = trim((string) $value);
|
|
if ($s === '') {
|
|
return '';
|
|
}
|
|
|
|
$maxLength = max(1, min($maxLength, 10000));
|
|
$s = mb_substr($s, 0, $maxLength);
|
|
|
|
// Control characters (keep tab/newline for multiline answers).
|
|
$s = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $s) ?? '';
|
|
|
|
$s = str_replace(["\0", '--', '/*', '*/', ';'], '', $s);
|
|
|
|
$patterns = [
|
|
'/\bunion\s+select\b/i',
|
|
'/\bselect\s+.+\s+from\b/i',
|
|
'/\binsert\s+into\b/i',
|
|
'/\bupdate\s+.+\s+set\b/i',
|
|
'/\bdelete\s+from\b/i',
|
|
'/\bdrop\s+(table|database)\b/i',
|
|
'/\bexec(\s|\()/i',
|
|
'/\bxp_\w+/i',
|
|
'/\bor\s+1\s*=\s*1\b/i',
|
|
'/\band\s+1\s*=\s*1\b/i',
|
|
"/'\s*or\s+'/i",
|
|
'/"\s*or\s+"/i',
|
|
'/\bchar\s*\(/i',
|
|
'/\bconcat\s*\(/i',
|
|
'/\bsleep\s*\(/i',
|
|
'/\bbenchmark\s*\(/i',
|
|
];
|
|
foreach ($patterns as $pattern) {
|
|
$s = preg_replace($pattern, '', $s) ?? $s;
|
|
}
|
|
|
|
return trim(preg_replace('/\s{2,}/u', ' ', $s) ?? '');
|
|
}
|
|
|
|
/**
|
|
* Returns sanitized text or calls json_error when nothing safe remains.
|
|
*/
|
|
function require_safe_free_text(mixed $value, string $fieldName, int $maxLength = 2000): string {
|
|
$raw = is_string($value) || is_numeric($value) ? trim((string) $value) : '';
|
|
if ($raw === '') {
|
|
json_error('INVALID_FIELD', "$fieldName is required", 400);
|
|
}
|
|
$clean = sanitize_free_text($raw, $maxLength);
|
|
if ($clean === '') {
|
|
json_error('INVALID_FIELD', "$fieldName contains disallowed content", 400);
|
|
}
|
|
return $clean;
|
|
}
|