Skip to content

feat(waf): evaluate the request body in WAF rules (body phase)#361

Merged
pigri merged 1 commit into
mainfrom
feat/waf-body-inspection
Jun 3, 2026
Merged

feat(waf): evaluate the request body in WAF rules (body phase)#361
pigri merged 1 commit into
mainfrom
feat/waf-body-inspection

Conversation

@pigri
Copy link
Copy Markdown
Contributor

@pigri pigri commented Jun 3, 2026

Summary

The request-phase WAF runs in request_filter, before the request body has streamed in, and was called with an empty body (b""). As a result every managed rule's http.request.body clause was effectively dead — SQLi, XSS, insecure-deserialization, webshell, and React-RCE body detections never matched regardless of payload. (The buffered body that does exist only fed the content scanner / IDS, not the wirefilter WAF rules.)

This wires the request body into the WAF:

  • Only buffers when needed. HttpFilter now tracks needs_request_body (set in compile_rules whenever a compiled request rule references http.request.body/body_sha256), exposed via request_rules_need_body(). When no rule reads the body, the zero-copy fast path is untouched.
  • Body-phase eval. New synchronous should_block_request_body_phase() re-evaluates the request rules against the buffered body, reusing the threat-intel response already fetched in the header phase (no re-fetch). Proxy wrappers: evaluate_waf_for_pingora_request_body_phase() + waf_request_rules_need_body().
  • Truncate, don't skip. request_body_filter buffers up to WAF_BODY_INSPECT_CAP (128 KiB) and inspects the head, rather than skipping oversized bodies. Attack markers appear early, so the head is what matters. The route-level max_body_size (hard 413) remains a separate, stricter cap applied first.
  • Block path. A block hit emits a BlockSource::Waf event and returns a proper 403 WAF block page (X-WAF-Rule / X-WAF-Rule-ID). challenge/ratelimit are not enforced post-body (they can't be cleanly applied once the body has streamed upstream).

Known limitation

Inspection happens post-stream (at end_of_stream), consistent with the existing content-scan/IDS design that forwards chunks to avoid the HTTP/2 HEADERS-only race. So for chunked uploads the origin may receive the request before the client gets the 403. True pre-origin blocking would require buffering in request_filter — intentionally out of scope here.

Note on the cap

WAF_BODY_INSPECT_CAP is a constant (128 KiB). The content scanner's analogous limit is config-driven; happy to promote this to proxy.waf.max_body_inspect_bytes if preferred.

Test plan

  • cargo test -p synapse-waf — 50/50 pass, incl. 3 new tests:
    • needs_request_body_tracks_body_field_usage (buffering decision)
    • body_phase_blocks_on_body_only_rule (body rule blocks in body phase, not empty header phase)
    • managed_ruleset_representative_rules_all_compile (parse-coverage probe over the managed catalog)
  • cargo check -p synapse-waf --features proxy + cargo check -p synapse-proxy
  • cargo fmt --check + cargo clippy --tests clean on both crates
  • cargo build --release --bin synapse
  • Manual: exercise a body-based managed rule (e.g. OWASP-A03 SQLi in a POST body) against a running proxy and confirm a 403 with X-WAF-Rule

The request-phase WAF ran in request_filter with an empty body, so every
managed rule's `http.request.body` clause was dead — SQLi/XSS/insecure-
deserialization/webshell/React-RCE body detections never fired regardless
of payload.

Buffer the request body only when a loaded request rule actually reads it
(`needs_request_body`, exposed via `request_rules_need_body()`), then at
end_of_stream re-evaluate the request rules against the body truncated to a
128 KiB inspection cap (`WAF_BODY_INSPECT_CAP`) — inspecting the head beats
skipping oversized bodies. The body-phase eval reuses the threat-intel
signal fetched in the header phase (no re-fetch). A `block` hit emits a
BlockSource::Waf event and returns a 403 WAF block page; challenge/ratelimit
aren't enforced post-body. Inspection is best-effort post-stream, consistent
with the existing content-scan/IDS path (the proxy forwards chunks to avoid
the h2 HEADERS-only race).

Adds tests: body-dependence tracking, body-phase block vs. empty header
phase, and a managed-ruleset parse-coverage probe.
@pigri pigri merged commit b9141d1 into main Jun 3, 2026
36 checks passed
@pigri pigri deleted the feat/waf-body-inspection branch June 3, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant