feat(waf): evaluate the request body in WAF rules (body phase)#361
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'shttp.request.bodyclause 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:
HttpFilternow tracksneeds_request_body(set incompile_ruleswhenever a compiled request rule referenceshttp.request.body/body_sha256), exposed viarequest_rules_need_body(). When no rule reads the body, the zero-copy fast path is untouched.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().request_body_filterbuffers up toWAF_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-levelmax_body_size(hard 413) remains a separate, stricter cap applied first.blockhit emits aBlockSource::Wafevent and returns a proper 403 WAF block page (X-WAF-Rule/X-WAF-Rule-ID).challenge/ratelimitare 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 inrequest_filter— intentionally out of scope here.Note on the cap
WAF_BODY_INSPECT_CAPis a constant (128 KiB). The content scanner's analogous limit is config-driven; happy to promote this toproxy.waf.max_body_inspect_bytesif 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-proxycargo fmt --check+cargo clippy --testsclean on both cratescargo build --release --bin synapseX-WAF-Rule