WAF-lite filtering
Dwara includes a lightweight web application filter (WAF-lite) that inspects incoming requests for common attack signatures: SQL injection, XSS, and path traversal. It is a heuristic pattern-matching filter, not a full WAF (Web Application Firewall) — it catches obvious attack payloads before they reach your upstream, with a bounded inspection cost and a dry-run mode for safe rollout.
How it works
The WAF runs after the route method allowlist and before the route limits, inspecting the ORIGINAL request (before path rewrite or transforms):
When to use this
WAF-lite is a first-line defense that catches obvious attack payloads (SQL injection (an attack that injects SQL via user input), XSS (Cross-Site Scripting — injecting browser-executed script), and path traversal (escaping a directory with ../ sequences)) before they reach the upstream, with a dry-run mode for safe rollout. It is NOT a full WAF — pair it with upstream input validation. It is per-route opt-in, so you can enable it on the routes that accept untrusted input and leave it off elsewhere.
Enabling the WAF
The WAF is per-route opt-in. Add a waf block to any route:
routes:
- name: api
match: { path: { type: prefix, value: /api } }
action: { type: proxy }
waf:
enabled: trueWith just enabled: true, all three filter categories run on every matching request, inspecting the path, query string, selected headers (User-Agent, Referer, Cookie, X-Forwarded-For), and body (when JSON or form-urlencoded (application/x-www-form-urlencoded, the default web form format), up to 128 KiB). A match returns 403 waf_blocked.
Filter categories
Choose which categories to run with the filters list:
waf:
enabled: true
filters: [sqli, xss]| Filter | What it catches |
|---|---|
sqli | UNION SELECT, OR 1=1, '; DROP TABLE, -- comments, xp_cmdshell, hex-encoded keywords, stacked queries, time-based blind injection |
xss | <script>, javascript:, onerror=, <iframe>, document.cookie, eval(, HTML entity-encoded variants, <svg onload> |
path_traversal | ../, ..\, %2e%2e%2f, double URL-encoding, null byte (%00), /etc/passwd, C:\Windows\ |
Omit filters or leave it empty to run all three (the default).
Dry-run mode (audit-log-only)
Before enforcing the WAF, observe what it would catch with dry_run: true:
waf:
enabled: true
dry_run: trueIn dry-run mode, the WAF evaluates every filter and logs matches but does NOT block — the request continues to your upstream. Check the dwara_waf_total{outcome="logged"} counter and the dwara::policy warn events to see what would have been blocked. Once you are satisfied with the false-positive rate, switch to dry_run: false (or remove the field) to enforce.
Body inspection
The WAF inspects request bodies when the content type is JSON, form-urlencoded, or text/plain, up to max_body_inspect_bytes:
waf:
enabled: true
max_body_inspect_bytes: 65536 # 64 KiB- Default: 131072 (128 KiB).
0: disable body inspection entirely (the body streams through untouched).- Maximum: 1048576 (1 MiB).
Bodies larger than the cap are inspected only up to the cap. A malicious payload beyond the cap is not caught — this is the trade-off for a bounded inspection cost.
Custom patterns
Add your own regex (a pattern-matching language) patterns alongside the built-in signatures:
waf:
enabled: true
custom_patterns:
- "(?i)internal_api_key_\\d+"
- "(?i)\\bssrf\\b"Custom patterns are appended to every enabled filter category. Invalid regexes are rejected at config validation time (the config will not publish).
Metrics
The dwara_waf_total{route,filter,outcome} counter tracks every WAF inspection:
| Outcome | Meaning |
|---|---|
blocked | A match was found and the request was rejected with 403. |
logged | A dry-run match (the request was allowed). |
passed | No match (counted once per inspected request, filter="all"). |
Request-path position
The WAF runs after the route method allowlist and before the route limits — a content filter that rejects malicious requests before any resource is spent on authentication or rate limiting. It inspects the ORIGINAL request (before path rewrite or transforms).
Limitations
- The WAF is heuristic, not semantic. It catches common attack signatures but cannot detect novel or obfuscated attacks that a full WAF with a rules engine would catch.
- Body inspection buffers up to
max_body_inspect_bytes. This is the one explicitly buffering piece the WAF introduces; the rest of the dataplane streams untouched. - The WAF does not inspect response bodies (it is a request-side filter only).
- The WAF does not replace authentication, authorization, or input validation in your upstream — it is a first-line defense that reduces the attack surface reaching your backend.
Runnable demo
Throw attack payloads at a live gateway: demos/04-security-auth/ (test script: test-13-waf-lite.sh) in the repository sends SQLi, XSS, and path-traversal inputs (asserting 403) alongside a clean request (200). The category README covers prerequisites and teardown.
Global CRS-compatible WAF
In addition to the per-route WAF-lite, the gateway supports a global CRS-compatible WAF policy configured at the gateway level. This is a rule-based engine with OWASP Core Rule Set concepts: rule IDs, severity levels, phases, tags, transformations, anomaly scoring, paranoia levels, and rule exclusions.
Configuration
waf:
enabled: true
dry_run: false
paranoia_level: 1
anomaly_threshold: 5
max_body_inspect_bytes: 131072
rules:
- id: 900001
severity: 2
phase: 1
tags: [SQL_INJECTION, OWASP_CRS]
pattern: "(?i)union\\s+select"
targets: [path, query]
transformations: [lowercase, url_decode]
exclude_rule_ids: [900002]
exclude_tags: [PARANOID]How it works
- Rules: each rule has a numeric ID, severity (1-4), phase (1 = request headers, 2 = request body), tags, a regex pattern, targets (path, query, headers, body), and transformations.
- Transformations: applied before pattern matching — lowercase, url_decode, html_entity_decode, compress_whitespace, remove_whitespace, url_decode_uni.
- Anomaly scoring: each matching rule adds its severity to the request's anomaly score. The request is blocked when the total reaches the
anomaly_threshold. - Exclusions: rules can be excluded by ID or tag, useful for tuning false positives without modifying the rule set.
- Dry run: when enabled, matches are logged but the request continues — useful for measuring the false-positive rate before enforcing.
The global WAF runs alongside the per-route WAF-lite. Both are opt-in (default off).