Request Threat Scanner
The siem.scan middleware runs lightweight pattern heuristics over the query string, URL and request body, flagging payloads that look like injection probes. It is telemetry, not a WAF: by default nothing is blocked — the SOC gets a suspicious_input event and the request proceeds.
Why detection-only by default? Because the interesting question isn't "did we block one probe" (your framework's parameter binding and escaping should make most probes inert) — it's "who is probing us, with what, and where". That's a SIEM question.
Usage#
// Broad deployment
$middleware->web(append: [\Inqaba\Gatekeeper\Http\Middleware\ScanRequestThreats::class]);
// Or targeted
Route::middleware('siem.scan')->group(...);
Detection rules#
| Rule | Catches |
|---|---|
sql_injection |
UNION SELECT, boolean tautologies with comment suffixes, SLEEP() / BENCHMARK() / WAITFOR DELAY / pg_sleep() time-based probes |
xss_probe |
<script>, event-handler attributes (onerror=, onload=, ...), javascript: URLs, document.cookie access, <iframe> / <svg onload> |
path_traversal |
repeated ../ sequences (raw and URL-encoded), /etc/passwd, C:\Windows |
command_injection |
shell metacharacters chained to commands (; cat, | whoami, `id`, $(curl ...)), ${IFS} |
template_injection |
{{ 7*7 }}-style SSTI probes, {{ config }} / {{ self }} Twig/Blade probing |
jndi_injection |
${jndi:ldap://...} log4shell-style payloads |
php_wrapper |
php://filter, php://input, expect://, phar:// stream wrappers |
null_byte |
%00 and escaped null-byte injection |
Each flagged input produces one finding (first matching rule wins), reported by input key, never value — the SIEM learns that q contained a SQLi probe, not the probe itself (see Privacy):
{
"event": { "action": "suspicious_input", "severity": 7 },
"threat": { "technique": { "id": ["T1190"], "name": ["Exploit Public-Facing Application"] } },
"laravel": {
"context": {
"findings": [
{ "rule": "sql_injection", "input": "q" },
{ "rule": "path_traversal", "input": "__url_path" }
]
}
}
}
__url_path means the match was in the request URI itself rather than an input field.
Blocking mode#
SIEM_SCANNER_BLOCK=true
Flagged requests are rejected with 400 (JSON-aware). Turn this on only after watching the suspicious_input stream for false positives in your traffic — a code-snippet field on a developer forum will trip xss_probe legitimately.
Avoiding false positives#
except_inputs— input keys skipped entirely (matched on the leaf key, souser.passwordis covered bypassword). Defaults:password,password_confirmation. Add any field that legitimately contains markup, code or SQL:
'scanner' => [
'except_inputs' => ['password', 'password_confirmation', 'body_html', 'code_sample'],
],
max_value_length(default 8192) — only the first N characters of each value are scanned, bounding CPU on large payloads.- Nested input is flattened with dot notation (
items.0.note), so findings pinpoint the exact field in array payloads.
Configuration#
| Key | Env | Default |
|---|---|---|
scanner.block |
SIEM_SCANNER_BLOCK |
false |
scanner.except_inputs |
— | password, password_confirmation |
scanner.max_value_length |
— | 8192 |
Honest limitations#
Pattern heuristics catch probes — the noisy reconnaissance phase where attackers spray known payloads. A skilled attacker crafting an app-specific exploit can evade regex. That's fine: the scanner's job is early warning and attribution ("IP X is testing our search parameter for SQLi"), layered under parameterized queries, escaping and a real WAF — not replacing them.