FEAT_SANITIZATION
This commit is contained in:
parent
cd90e68a31
commit
5cd6cfb7fa
@ -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',
|
||||
],
|
||||
|
||||
@ -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()
|
||||
// ]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
130
app/Filters/SecurityInputFilter.php
Normal file
130
app/Filters/SecurityInputFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user