Over HTTP/3, HAProxy 3.0's req.body_size equals the bytes buffered, because the H3 mux never sets htx->extra from Content-Length. As a result, any body cut at one buffer (~15 KB) was stamped X-WHP-WAF-Body: complete. Coraza then failed to parse the truncated JSON (200002), and WHP rule 991900020 returned a 403.
whp01, Oct 6-19: 73,795 H3 complete stamps and 0 partial. 7 legitimate requests were 403'd: 6 Divi sync-to-server saves of 265-417 KB and 1 Groundhogg preview of 18 KB.
Fix: take the advertised length from req.hdr_val(content-length). H1, H2 and 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.
Tests:scripts/test-waf-body-inspection.py passes 28/28. With the template reverted, 2 tests fail.
Lab: verified on haproxy 3.0.11 with QUIC. 18 KB, 265 KB and 417 KB bodies are stamped partial on H1, H2 and H3; bodies of 14 KB or less are stamped complete.
Deploy: rebuild the image, then recreate haproxy-manager per host, .148 first.
Over HTTP/3, HAProxy 3.0's `req.body_size` equals the bytes buffered, because the H3 mux never sets `htx->extra` from Content-Length. As a result, any body cut at one buffer (~15 KB) was stamped `X-WHP-WAF-Body: complete`. Coraza then failed to parse the truncated JSON (200002), and WHP rule 991900020 returned a 403.
- **whp01, Oct 6-19:** 73,795 H3 `complete` stamps and 0 `partial`. 7 legitimate requests were 403'd: 6 Divi `sync-to-server` saves of 265-417 KB and 1 Groundhogg preview of 18 KB.
- **Fix:** take the advertised length from `req.hdr_val(content-length)`. H1, H2 and 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.
- **Tests:** `scripts/test-waf-body-inspection.py` passes 28/28. With the template reverted, 2 tests fail.
- **Lab:** verified on haproxy 3.0.11 with QUIC. 18 KB, 265 KB and 417 KB bodies are stamped `partial` on H1, H2 and H3; bodies of 14 KB or less are stamped `complete`.
**Deploy:** rebuild the image, then recreate haproxy-manager per host, .148 first.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Over HTTP/3, HAProxy 3.0's
req.body_sizeequals the bytes buffered, because the H3 mux never setshtx->extrafrom Content-Length. As a result, any body cut at one buffer (~15 KB) was stampedX-WHP-WAF-Body: complete. Coraza then failed to parse the truncated JSON (200002), and WHP rule 991900020 returned a 403.completestamps and 0partial. 7 legitimate requests were 403'd: 6 Divisync-to-serversaves of 265-417 KB and 1 Groundhogg preview of 18 KB.req.hdr_val(content-length). H1, H2 and 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.scripts/test-waf-body-inspection.pypasses 28/28. With the template reverted, 2 tests fail.partialon H1, H2 and H3; bodies of 14 KB or less are stampedcomplete.Deploy: rebuild the image, then recreate haproxy-manager per host, .148 first.
🤖 Generated with Claude Code