fix(waf): stamp X-WHP-WAF-Body from the Content-Length header, not req.body_size

req.body_size is "buffered + htx->extra". In HAProxy 3.0 only the H1 and
H2 muxes set htx->extra from Content-Length; the H3 mux never does, so
over HTTP/3 req.body_size == req.body_len and any body cut at one buffer
(~15 KB) was stamped `complete`. Coraza then failed to parse the
truncated JSON (200002) and WHP's 991900020 returned a 403.

Measured on whp01, Oct 6-19 2026: 73,795 H3 `complete` stamps and 0
`partial`; 7 legitimate requests 403'd (6 Divi sync-to-server saves of
265-417 KB, 1 Groundhogg preview of 18 KB). Reproduced in a lab on
haproxy 3.0.11 with QUIC. H1/H2/H3 all reject a Content-Length that
doesn't match the DATA received, so the header is a protocol-independent
body length. No change on H1/H2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-19 08:59:29 -07:00
1 parent cbbd15b787
commit 2cc6790db1
2 files changed
+26 -4

No files matched your search

+14 -1
View File
@@ -92,7 +92,7 @@ class ListenerWithCoraza(unittest.TestCase):
def test_stamp_order_strip_set_inspect_strip(self):
strip1 = index_of(self.lines, r'^http-request del-header X-WHP-WAF-Body$')
size = index_of(self.lines, r'^http-request set-var\(txn\.waf_body_size\) req\.body_size if \{ req\.hdr_cnt\(content-length\) eq 1 \}$')
size = index_of(self.lines, r'^http-request set-var\(txn\.waf_body_size\) req\.hdr_val\(content-length\) if \{ req\.hdr_cnt\(content-length\) eq 1 \}$')
complete = index_of(self.lines, r'^http-request set-var\(txn\.waf_body\) str\(complete\) if \{ var\(txn\.waf_body_size\) -m found \} \{ req\.body_len,sub\(txn\.waf_body_size\) eq 0 \}$')
partial = index_of(self.lines, r'^http-request set-var\(txn\.waf_body\) str\(partial\) if \{ var\(txn\.waf_body_size\) -m found \} !\{ req\.body_len,sub\(txn\.waf_body_size\) eq 0 \}$')
hdrs_len = index_of(self.lines, r'^http-request set-var\(txn\.waf_hdrs_len\) req\.hdrs,length if !\{ var\(txn\.waf_body_size\) -m found \} \{ req\.body_len gt 0 \}$')
@@ -137,6 +137,19 @@ class ListenerWithCoraza(unittest.TestCase):
bufsize = int([l for l in header if l.startswith('tune.bufsize')][0].split()[1])
self.assertLessEqual(total_lt, (bufsize - 1024) * 0.85)
def test_stamp_never_derived_from_req_body_size(self):
"""req.body_size is "buffered + htx->extra" and HAProxy 3.0's H3 mux
never sets htx->extra, so over HTTP/3 it equals req.body_len and a
body truncated at one buffer reads as `complete` (measured on whp01
2026-10-17: six Divi sync-to-server saves of 252-417 KB 403'd by
WHP 991900020; lab-reproduced on haproxy 3.0.11 +QUIC). The
advertised length must come from the Content-Length header."""
uses = [l for l in self.lines if 'req.body_size' in l]
self.assertEqual(uses, [], 'req.body_size must not feed the X-WHP-WAF-Body stamp')
size = [l for l in self.lines if l.startswith('http-request set-var(txn.waf_body_size) ')]
self.assertEqual(len(size), 1)
self.assertIn('req.hdr_val(content-length)', size[0])
def test_complete_and_partial_are_mutually_exclusive_conditions(self):
"""With a Content-Length, `complete` and `partial` must be the same
condition, negated -- an inverted or loosened `complete` would enforce