131 lines
3.6 KiB
PHP
131 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace App\Filters;
|
|
|
|
use CodeIgniter\Filters\FilterInterface;
|
|
use CodeIgniter\HTTP\RequestInterface;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
use Config\Services;
|
|
|
|
class SecurityInputFilter implements FilterInterface
|
|
{
|
|
/**
|
|
* High-confidence XSS patterns only
|
|
* (low false-positive set)
|
|
**/
|
|
|
|
protected array $xssPatterns = [
|
|
// Script execution
|
|
'/<\s*script\b/i',
|
|
'/<\/\s*script\s*>/i',
|
|
|
|
// JavaScript execution vectors
|
|
'/javascript\s*:/i',
|
|
'/vbscript\s*:/i',
|
|
'/data\s*:\s*text\/html/i',
|
|
|
|
// Inline event handlers (strong signal)
|
|
'/on\w+\s*=\s*["\']?/i',
|
|
|
|
// Dangerous HTML tags
|
|
'/<\s*iframe\b/i',
|
|
'/<\s*object\b/i',
|
|
'/<\s*embed\b/i',
|
|
'/<\s*applet\b/i',
|
|
'/<\s*img\b/i',
|
|
|
|
// Image-based execution
|
|
'/<\s*img\b[^>]*on\w+/i',
|
|
|
|
// SVG-based execution (modern bypass)
|
|
'/<\s*svg\b/i',
|
|
'/<\s*math\b/i',
|
|
|
|
// Meta refresh redirect
|
|
'/<\s*meta\b[^>]*http-equiv\s*=\s*["\']?refresh/i',
|
|
|
|
// HTML injection via src/href
|
|
'/<\s*\w+\b[^>]*(src|href)\s*=\s*["\']?\s*(javascript|data)\s*:/i'
|
|
];
|
|
|
|
|
|
public function before(RequestInterface $request, $arguments = null)
|
|
{
|
|
$logger = Services::mylogger();
|
|
$response = Services::response();
|
|
|
|
// Collect all user-controlled input
|
|
$inputs = array_merge(
|
|
$request->getGet(),
|
|
$request->getPost()
|
|
);
|
|
|
|
if (empty($inputs)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($inputs as $field => $value) {
|
|
if (is_array($value)) {
|
|
$value = json_encode($value);
|
|
}
|
|
|
|
// Step 1: Canonicalization (VERY IMPORTANT)
|
|
$canonical = $this->canonicalize($value);
|
|
|
|
// Step 2: Trim (hygiene)
|
|
$canonical = trim($canonical);
|
|
|
|
// Step 3: Detection (signal-only)
|
|
if ($this->detectXss($canonical)) {
|
|
|
|
// 🔐 Log intent, not data
|
|
$logger->logme('critical','SECURITY_BLOCKED_REQUEST - '. json_encode([
|
|
'ip' => $request->getIPAddress(),
|
|
'method' => $request->getMethod(),
|
|
'uri' => current_url(),
|
|
'field' => $field,
|
|
'attack' => 'XSS_PATTERN',
|
|
'length' => strlen($canonical),
|
|
'hash' => hash('sha256', $canonical),
|
|
]));
|
|
|
|
// ⛔ Block request
|
|
return $response
|
|
->setStatusCode(403)
|
|
->setJSON([
|
|
'status' => 403,
|
|
'error' => 'Forbidden',
|
|
'message' => 'Malicious input detected'
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
|
{
|
|
// no-op
|
|
}
|
|
|
|
/**
|
|
* Canonicalization prevents encoded bypass
|
|
*/
|
|
private function canonicalize(string $value): string
|
|
{
|
|
$value = urldecode($value);
|
|
$value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
|
|
// Remove invisible control characters
|
|
return preg_replace('/[\x00-\x1F\x7F]/u', '', $value);
|
|
}
|
|
|
|
private function detectXss(string $value): bool
|
|
{
|
|
foreach ($this->xssPatterns as $pattern) {
|
|
if (preg_match($pattern, $value)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|