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

View File

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