WAF-lite signature/anomaly inspection
A full Web Application Firewall belongs at your edge — a CDN, reverse proxy, or ModSecurity with the OWASP Core Rule Set. DaloyJS does not try to replace that. But plenty of teams ship without an edge WAF, and for them waf() is a first-party, opt-in defense-in-depthlayer: it wires the framework's high-confidence injection signatures into a single, scored inbound-inspection pass you can turn on with one line.
As of 0.37.0, waf() inspects the decoded URL path, the raw and decoded query string, an optional header allowlist, and the validated request body for four rule categories — SQLi, XSS, NoSQLi (Mongo-style operator injection), and command injection. Each rule that fires contributes an anomaly score; when the total reaches the threshold, the request is rejected with a generic 403 (block mode) or merely reported (log mode).
Quick start
The middleware runs in the beforeHandle phase, so it sees the validated context — query, params, headers, and the schema-parsed body. Because body inspection reads ctx.body, it composes with the framework's schema-first contract: routes that declare a body schema are body-inspected automatically.
Tune in log mode first
Signatures are curated for a low false-positive rate, but every app is different. Start in "log" mode, watch onMatch against real traffic, then switch to "block" once you are confident.
onMatch fires once per actionable detection (score at or above the threshold) in both modes, immediately before any 403 is thrown. Each entry in event.matches carries the ruleId, the score it contributed, the location it matched (path, query, header, or body), and a short, control-character-stripped sample for your logs.
The rules
- sqli —
UNION SELECT, boolean tautologies (OR 1=1), stacked statements (; DROP TABLE), time-based probes (SLEEP(),WAITFOR DELAY),INFORMATION_SCHEMA,xp_cmdshell, and file primitives. - xss —
<script>tags,javascript:URIs, inline event handlers (onerror=,onload=), anddocument.cookieexfiltration. - nosqli — Mongo operator strings (
$ne,$where, …) and a structural check that rejects a parsed body containing any$-prefixed key, so{"password": {"$ne": null}}is caught even when no string value matches. - cmdi — shell metacharacters chaining into binaries (
; rm,| nc,&& curl), command substitution ($(...), backticks), and sensitive path access (/etc/passwd).
Scoring and the block threshold
Each rule contributes its score once per request(deduplicated across all inspected locations). The default score is 5 and the default blockThreshold is 5, so any single high-confidence signature trips the guard. Raise the threshold to require multiple independent categories before acting:
Per-rule enable/disable
Disable a noisy rule with a boolean, or pass an object to enable it with a custom score. Omitted rules keep their defaults (enabled, score 5).
Inspection scope
Path, query, and body are inspected by default. Header inspection is opt-in and requires an explicit allowlist, because common headers (User-Agent, Cookie, Referer) carry punctuation that can trip signatures.
Scanning is bounded so a hostile payload cannot turn inspection into CPU-DoS: maxValueLength (default 8192) caps the length of any single scanned string, and maxBodyNodes (default 10000) caps how many body nodes are walked. Only own enumerable properties are followed — prototype keys are never inspected.
Security notes
- The
403body is intentionally generic (Request blocked by security policy) — it never tells an attacker which signature fired. Rule detail is delivered server-side viaonMatchonly. - This is a complement to input schemas and parameter binding, not a substitute. Keep validating with Zod schemas; the WAF is a second line for traffic that slips through application logic.
- Routes without a body schema are not body-inspected (their body is never parsed). Add a schema to bring their inputs under coverage.
- A WAF-lite is best-effort signature matching: determined attackers can craft evasions. Treat it as depth, and keep an edge WAF on your roadmap for high-risk surfaces.