FEAT_SANITIZATION

This commit is contained in:
velz 2026-01-08 16:02:00 +05:30
parent cd90e68a31
commit 5cd6cfb7fa
3 changed files with 166 additions and 33 deletions

View File

@ -17,6 +17,7 @@ use App\Filters\VerifyAppSignature;
use App\Filters\AuthJWT; use App\Filters\AuthJWT;
use App\Filters\Cors; use App\Filters\Cors;
use App\Filters\GlobalPostFileUploadGuard; use App\Filters\GlobalPostFileUploadGuard;
use App\Filters\SecurityInputFilter;
class Filters extends BaseConfig class Filters extends BaseConfig
{ {
@ -41,6 +42,7 @@ class Filters extends BaseConfig
'Cors' => Cors::class, 'Cors' => Cors::class,
'appSignature' => VerifyAppSignature::class, 'appSignature' => VerifyAppSignature::class,
'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class, 'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class,
'SecurityInputFilter' => SecurityInputFilter::class,
]; ];
@ -55,6 +57,7 @@ class Filters extends BaseConfig
'before' => [ 'before' => [
'HttpRequestLog' => ['except' => 'cli/*'], 'HttpRequestLog' => ['except' => 'cli/*'],
'Cors', 'Cors',
'SecurityInputFilter',
'GlobalPostFileUploadGuard', 'GlobalPostFileUploadGuard',
// 'invalidchars', // 'invalidchars',
], ],

View File

@ -107,11 +107,11 @@ class Cors implements FilterInterface
); );
} }
$this->log('CORS filter initialized', [ // $this->log('CORS filter initialized', [
'allowed_origins' => $this->allowedOrigins, // 'allowed_origins' => $this->allowedOrigins,
'allow_credentials' => $this->allowCredentials, // 'allow_credentials' => $this->allowCredentials,
'allowed_methods' => $this->allowedMethods, // 'allowed_methods' => $this->allowedMethods,
]); // ]);
} }
/** /**
@ -143,7 +143,7 @@ class Cors implements FilterInterface
// If wildcard present in configuration, allow any origin // If wildcard present in configuration, allow any origin
if (in_array('*', $this->allowedOrigins, true)) { if (in_array('*', $this->allowedOrigins, true)) {
$this->log('Origin allowed: wildcard match', ['origin' => $origin]); // $this->log('Origin allowed: wildcard match', ['origin' => $origin]);
return true; return true;
} }
@ -159,10 +159,10 @@ class Cors implements FilterInterface
// 1. Exact match (including scheme and port) // 1. Exact match (including scheme and port)
// Example: https://example.com matches https://example.com // Example: https://example.com matches https://example.com
if (strcasecmp($allowed, $origin) === 0) { if (strcasecmp($allowed, $origin) === 0) {
$this->log('Origin allowed: exact match', [ // $this->log('Origin allowed: exact match', [
'origin' => $origin, // 'origin' => $origin,
'matched_rule' => $allowed // 'matched_rule' => $allowed
]); // ]);
return true; return true;
} }
@ -178,11 +178,11 @@ class Cors implements FilterInterface
// Check if origin host ends with the allowed root domain // Check if origin host ends with the allowed root domain
if ($originHost === $allowedRoot || str_ends_with($originHost, '.' . $allowedRoot)) { if ($originHost === $allowedRoot || str_ends_with($originHost, '.' . $allowedRoot)) {
$this->log('Origin allowed: wildcard subdomain match', [ // $this->log('Origin allowed: wildcard subdomain match', [
'origin' => $origin, // 'origin' => $origin,
'matched_rule' => $allowed, // 'matched_rule' => $allowed,
'origin_host' => $originHost // 'origin_host' => $originHost
]); // ]);
return true; return true;
} }
} }
@ -191,11 +191,11 @@ class Cors implements FilterInterface
// Example: example.com matches both http://example.com and https://example.com // Example: example.com matches both http://example.com and https://example.com
else { else {
if (strcasecmp($allowed, $originHost) === 0) { if (strcasecmp($allowed, $originHost) === 0) {
$this->log('Origin allowed: host match (scheme-less)', [ // $this->log('Origin allowed: host match (scheme-less)', [
'origin' => $origin, // 'origin' => $origin,
'matched_rule' => $allowed, // 'matched_rule' => $allowed,
'origin_host' => $originHost // 'origin_host' => $originHost
]); // ]);
return true; return true;
} }
} }
@ -320,11 +320,11 @@ class Cors implements FilterInterface
// Preflight is sent by browsers before actual cross-origin requests // Preflight is sent by browsers before actual cross-origin requests
// to check if the actual request is safe to send // to check if the actual request is safe to send
if ($method === 'OPTIONS') { if ($method === 'OPTIONS') {
$this->log('Preflight request received', [ // $this->log('Preflight request received', [
'origin' => $origin, // 'origin' => $origin,
'method' => $method, // 'method' => $method,
'uri' => (string) $request->getUri() // 'uri' => (string) $request->getUri()
]); // ]);
// Validate origin - reject if not allowed // Validate origin - reject if not allowed
if (empty($origin) || !$this->isOriginAllowed($origin)) { if (empty($origin) || !$this->isOriginAllowed($origin)) {
@ -346,10 +346,10 @@ class Cors implements FilterInterface
$response->setStatusCode(204); $response->setStatusCode(204);
$response->setBody(''); $response->setBody('');
$this->log('Preflight approved', [ // $this->log('Preflight approved', [
'origin' => $origin, // 'origin' => $origin,
'allowed_methods' => $this->allowedMethods // 'allowed_methods' => $this->allowedMethods
]); // ]);
return $response; return $response;
} }
@ -392,10 +392,10 @@ class Cors implements FilterInterface
// Add CORS headers to the response // Add CORS headers to the response
$this->addCorsHeaders($response, $request, $origin, false); $this->addCorsHeaders($response, $request, $origin, false);
$this->log('CORS headers added to response', [ // $this->log('CORS headers added to response', [
'origin' => $origin, // 'origin' => $origin,
'status' => $response->getStatusCode() // 'status' => $response->getStatusCode()
]); // ]);
} }
/** /**

View File

@ -0,0 +1,130 @@
<?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;
}
}