Curl_compareheader() fails to match multi-value HTTP headers

Disclosed: 2026-03-12 15:51:43 By henriqueg To curl
Medium
Vulnerability Details
## Summary `Curl_compareheader()` in `lib/http.c` fails to scan the full value of HTTP headers for substring matches. Due to an incorrect loop condition, only the first byte position of the header value is checked. This causes curl to miss connection options like `close` when they appear as non-first tokens in comma-separated header values such as `Connection: upgrade, close`. When a malicious server sends `Connection: upgrade, close`, curl fails to detect the `close` token and incorrectly keeps the connection alive in its pool. A subsequent request to the same host:port reuses this connection, sending its credentials (Authorization headers, cookies) to the still-listening attacker server. ## Detailed Description The `Connection` header field is defined in RFC 9110 §7.6.1 with ABNF grammar `Connection = #connection-option`, where `#` denotes a comma-separated list. Therefore `Connection: upgrade, close` is a valid header containing two connection options. RFC 9112 §9.6 states: > A client that receives a "close" connection option MUST cease sending requests on that connection and close the connection after reading the response message containing the "close" connection option. The bug is in the `for` loop at `lib/http.c` line 1438: ```c for(len = curlx_strlen(&val); len >= curlx_strlen(&val); len--, p++) { ``` The loop initializes `len` to the value length and then checks `len >= curlx_strlen(&val)` - the same value. After the first iteration and `len--`, the condition is false and the loop exits. Only position 0 is ever checked. The same function is used for matching `Connection: close`, `Connection: keep-alive`, `Transfer-Encoding: chunked`, `Expect: 100-continue`, and proxy `Connection` / `Proxy-Connection` headers. All are affected. No other major HTTP client has this bug. wget, Python `requests`, Python `http.client`, and Node.js `http` all correctly parse multi-value `Connection` headers. ## Impact ## Impact A malicious server can exploit this to steal credentials from subsequent requests via connection pool poisoning: 1. Attacker's server responds to a victim request with `Connection: upgrade, close` 2. curl misses the `close` token and keeps the connection in its pool 3. Attacker keeps the TCP socket open (violating its own `close` declaration) 4. Victim's next request to the same host:port reuses the poisoned connection 5. The request - including `Authorization` headers and cookies - sent to the attacker's still-listening socket This is exploitable in environments where curl makes sequential requests to the same host:port, such as API gateways, Kubernetes services behind shared Ingress, CI/CD pipelines, and shared hosting. ### PoC - Exploitation The PoC server (`server.py`) simulates two services behind the same host:port: - `/public` - attacker-controlled endpoint that poisons the connection - `/private` - legitimate endpoint that requires `Authorization` and returns account data **Test 1 - Direct request to /private (safe baseline):** ```sh # Terminal 1 python3 server.py 8000 # Terminal 2 curl -s http://127.0.0.1:8000/private \ -H "Authorization: Bearer SECRET_TOKEN" ``` Output: ``` [private] Auth received: Authorization: Bearer SECRET_TOKEN [private] Returned account data (normal flow) {"account": "[email protected]", "balance": 42000} ``` Credentials go to the legitimate `/private` handler. No leak. This is the expected behavior. **Test 2 - Visit /public first, then /private (attack):** ```sh # Terminal 1 python3 server.py 8000 # Terminal 2 curl -s http://127.0.0.1:8000/public \ --next \ -H "Authorization: Bearer SECRET_TOKEN" \ http://127.0.0.1:8000/private ``` Output: ``` [attacker] Request from 127.0.0.1: GET /public HTTP/1.1 [attacker] Sent 'Connection: upgrade, close' [attacker] Waiting for leaked request on same socket... [attacker] *** INTERCEPTED REQUEST *** GET /private HTTP/1.1 Host: 127.0.0.1:8000 User-Agent: curl/8.19.0-DEV Accept: */* >>> Authorization: Bearer SECRET_TOKEN [attacker] This request was meant for /private. [attacker] The credentials were leaked because curl [attacker] reused the poisoned connection. ``` Request #1 to `/public` had NO credentials. Request #2 to `/private` carried `Authorization: Bearer SECRET_TOKEN`. But the `/private` handler never ran - the attacker's `/public` handler captured the credentials on the reused TCP socket. PS. The `--next` flag resets per-request options so `Authorization` applies only to the second URL.
Actions
View on HackerOne
Report Stats
  • Report ID: 3598444
  • State: Closed
  • Substate: informative
  • Upvotes: 1
Share this report