HexBrain

curl · Report #4053116

curl CONNECT response header colon-check bypass in cf-h1-proxy single_header — proxy authentication bypass via injected HTTP/1.x status line

N/Astatus
highseverity
9upvotes
19 Oct 2026disclosed

How it was found

Summary

In the HTTP/1.1 proxy-CONNECT response parser (lib/cf-h1-proxy.c, single_header()), the per-line counter data->req.headerline (a.k.a. k->headerline) is never incremented — only the tunnel-local ts->headerlines is. Curl_req_soft_reset() resets k->headerline to 0 before the CONNECT exchange begins, and it stays 0 for the entire CONNECT response. Curl_verify_header() (lib/http.c) treats k->headerline < 2 as "this is a status line" and returns CURLE_OK without performing its colon-presence check, so that colon check — which exists precisely to reject garbage/injected lines in the header area — is dead code on the CONNECT path. The normal HTTP response path (http_rw_hd) increments k->headerline (if(!k->headerline++)), so the guard works there and only there.

Because the colon check is inert on the CONNECT path, a malicious proxy can inject a line shaped like an HTTP/1.x status line into the header area of a 407 Proxy Authentication Required response. on_resp_header() matches any line beginning with HTTP/1. (without checking whether it is the first/status line, and without requiring a colon) and overwrites k->httpcode and data->info.httpproxycode with the parsed status code. Injecting HTTP/1.1 200 OK: x overwrites the 407 with 200. The end-of-headers empty-line handler then sees httpcode == 200 (not 407) and proceeds to KEEPON_DONE; recv_CONNECT_resp() sees httpproxycode/100 == 2 and skips Curl_http_auth_act(); H1_CONNECT() sees the same 2xx and marks the tunnel H1_TUNNEL_ESTABLISHED. The client believes the tunnel is up, never retries with proxy authentication, and sends the real request — Cookie, Authorization, POST bodies, and the Proxy-Authorization header from the CONNECT request — over the attacker's connection.

The injected line must carry a colon (e.g. HTTP/1.1 200 OK: x) to satisfy the secondary colon check in Curl_headers_push() → namevalue(). This does not change the root cause; it only shapes the payload.

Impact variant (all verified end-to-end)Target schemeWhat the attacker captures / controls
Plaintext request leakhttp:// targetFull request: method, Host, Cookie, Authorization, POST body; attacker can also return a forged response (integrity)
TLS ClientHello intercepthttps:// targetTLS ClientHello incl. SNI and connection metadata; TLS content stays encrypted; attacker may send a TLS alert to kill the connection (availability)
Proxy-credential captureany CONNECTProxy-Authorization: Basic from the CONNECT request, base64-decoded to user:password
No-auth-configured hijackany target, no --proxy-userTunnel still marked established; plaintext HTTP request captured

Affected versions

  • curl 8.21.0-DEV (mbedTLS 3.6.5 debug build).
  • Current master 9b2caf3 (8.22.0-DEV, verified 2026-08-31). Still affected. all five verification checks pass; verbose output is byte-for-byte identical to 8.21.0-DEV: the 407 is overwritten to 200 by the injected line, * CONNECT tunnel established, response 200 is printed, and the tunneled request carrying Cookie: secret=session123 + Authorization: Bearer sensitive_token is captured (curl/8.22.0-DEV User-Agent).
  • lib/cf-h1-proxy.c single_header() (master line 474) still only does ts->headerlines++; data->req.headerline is never incremented, and k is a const pointer (line 470) so the existing code cannot mutate it. No fix has landed.

PoC

The attacker operates a malicious HTTP proxy (or is a man-in-the-middle between the curl client and the legitimate proxy — curl's documented attacker capability "B"). The client uses curl with --proxy-tunnel (-p) or simply requests an https:// target through the proxy (which forces a CONNECT), and has proxy authentication configured (--proxy-user / CURLOPT_PROXYUSERPWD). The client's request also carries sensitive headers (Cookie, Authorization).

  1. The client sends CONNECT <host>:<port> HTTP/1.1 to the attacker's proxy, including a Proxy-Authorization: Basic <base64(user:pass)> header.
  2. The attacker responds with a 407 challenge and, in the header area, an injected line:
    HTTP/1.1 407 Proxy Authentication Required\r\n
    Proxy-Authenticate: Basic realm="proxy"\r\n
    HTTP/1.1 200 OK: x\r\n
    Content-Length: 0\r\n
    \r\n
    
    The injected HTTP/1.1 200 OK: x carries a colon (required to pass the secondary namevalue() colon check) while still matching the HTTP/1. prefix pattern in on_resp_header().
  3. Curl_verify_header() returns CURLE_OK for the injected line (headerline == 0 < 2, colon check skipped).
  4. Curl_headers_push() → namevalue() accepts the line (it contains a colon).
  5. on_resp_header() matches the HTTP/1.x pattern and overwrites k->httpcode / data->info.httpproxycode from 407 to 200.
  6. At the empty line, httpcode is 200, so the handler takes the KEEPON_DONE branch instead of KEEPON_IGNORE.
  7. recv_CONNECT_resp() sees httpproxycode/100 == 2 and does not call Curl_http_auth_act() — no authentication retry, data->req.newurl is not set.
  8. H1_CONNECT() sees 2xx and marks the tunnel H1_TUNNEL_ESTABLISHED; cf_h1_proxy_connect sets cf->connected = TRUE.
  9. The client, believing the tunnel is established, sends the real request over the attacker's connection. For an http:// target this is the full plaintext request (Cookie, Authorization, POST body); for an https:// target it is the TLS ClientHello (SNI exposed, TLS content encrypted).

The negative control — the same injection without -p (normal HTTP proxy path through http_rw_hd, where k->headerline is incremented) — does not overwrite httpcode, does not establish a tunnel, and does not bypass authentication, confirming the bug is specific to the CONNECT response path. The full proof-of-concept script (malicious proxy) is available on request.

Impact

  • CVSS 3.1: 9.6 (Critical) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N.
  • Trigger conditions: The attacker must operate a malicious HTTP proxy or be positioned as a MITM between the client and the legitimate proxy (curl's documented attacker capability "B"). No privileges on the client host are required. The client must use CONNECT tunnelling (-p / --proxy-tunnel, or any https:// request through a proxy). The injected line must carry a colon to pass the secondary namevalue() check.
  • Realized impact:
    • http:// targets: full plaintext request captured — Cookie, Authorization, POST bodies, and all custom headers; the attacker can also return a forged HTTP response (integrity). Session hijacking and API-key/credential disclosure.
    • Proxy authentication is fully bypassed — even with CURLOPT_PROXYUSERPWD set, the auth retry never fires.
    • The Proxy-Authorization: Basic header from the CONNECT request is captured and base64-decoded to plaintext proxy credentials.
    • https:// targets: TLS ClientHello (incl. SNI) is intercepted; TLS-encrypted content is not exposed. The attacker may send a TLS alert to terminate the connection (availability), or complete a MITM if the client has disabled TLS verification (-k).
  • Severity bound: The C:H/I:H is fully realized for plaintext HTTP targets and for proxy-credential capture. For HTTPS targets the confidentiality impact is bounded to SNI/connection-metadata exposure (TLS content remains encrypted), and the availability impact is limited to connection termination. The attack requires the attacker to control the proxy or be a MITM; it does not grant the attacker any new network position they did not already hold, but it defeats proxy authentication — the mechanism intended to gate access to the proxy and to the upstream — and turns a proxy that would have rejected the unauthenticated CONNECT into one that silently exfiltrates the client's request and credentials.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original