curl · Report #4124549
CURLOPT_PROXYHEADER is not part of the HTTP-proxy connection reuse key: a second CONNECT-tunnel transfer silently reuses the first tunnel's Proxy-Auth
How it was found
Summary
When libcurl tunnels through an HTTP proxy with CONNECT (HTTPS-over-proxy), the custom headers
set via CURLOPT_PROXYHEADER are sent to the proxy once, at tunnel-setup time
(lib/http_proxy.c:63-83). The connection-reuse decision for a proxied connection, however, does
not take proxyheaders into account. proxy_info_matches() compares only the proxy type, the
proxy destination, and Curl_creds:
/* lib/url.c:540-549 */
static bool proxy_info_matches(const struct proxy_info *data,
const struct proxy_info *needle)
{
if((data->proxytype == needle->proxytype) &&
Curl_peer_same_destination(data->peer, needle->peer) &&
Curl_creds_same(data->creds, needle->creds)) {
return TRUE;
}
return FALSE;
}
git grep proxyheaders lib/url.c returns nothing — the field is absent from the entire
reuse-matching path (url_match_proxy_use, url_match_*).
Consequently, two transfers to the same destination through the same proxy that differ only in
their CURLOPT_PROXYHEADER are treated as reusable. The second transfer rides the tunnel the first
established, and its own proxy headers are never presented to the proxy.
Why this is the CVE-2026-3784 class
CVE-2026-3784 ("wrong proxy connection reuse with credentials") established that proxy-side identity
must be part of the reuse key. Its fix covers Curl_creds (the CURLOPT_PROXYUSERPWD path). But a
very common way to pass proxy identity — especially bearer tokens and multi-tenant routing headers —
is CURLOPT_PROXYHEADER (Proxy-Authorization: Bearer ..., X-Tenant: ..., etc.). Those live
outside Curl_creds and were not added to the key, so the same wrong-reuse outcome is still
reachable through a different, un-keyed identity channel.
Attacker model / impact
A process (or application embedding libcurl) that issues multiple proxied transfers over a shared
connection pool — the normal case for one easy handle reused, a multi handle, or a CURLSH with
CURL_LOCK_DATA_CONNECT — where different transfers are meant to present different proxy
identities via CURLOPT_PROXYHEADER. A later transfer that should authenticate to the proxy as
identity B instead silently inherits the tunnel authenticated as identity A. In a multi-tenant
client this is a cross-tenant proxy-identity confusion; where the first tunnel was established with a
privileged Proxy-Authorization, a subsequent unprivileged transfer reuses that privileged tunnel.
PoC (live, with control)
attachments/poc_proxy_hdr_reuse.py stands up a local TLS origin and a local HTTP CONNECT proxy
that logs the headers of every CONNECT it receives. attachments/proxy_client.c (linked against the
libcurl built from this commit) performs two HTTPS-over-CONNECT transfers on one easy handle
(shared pool), differing only in CURLOPT_PROXYHEADER — here a real proxy credential:
Proxy-Authorization: Bearer TOKEN_A then Proxy-Authorization: Bearer TOKEN_B.
Captured output (attachments/proxy_hdr_transcript.txt):
################ EXPLOIT (fresh2=0): differing proxy headers, shared pool ################
CONNECT requests the proxy received : 1
Proxy-Authorization the proxy saw : ['Bearer TOKEN_A']
>>> transfer#2 reused the tunnel; its 'Proxy-Authorization: Bearer TOKEN_B' never reached the proxy
################ CONTROL (fresh2=1): transfer#2 FRESH_CONNECT ################
CONNECT requests the proxy received : 2
Proxy-Authorization the proxy saw : ['Bearer TOKEN_A', 'Bearer TOKEN_B']
>>> forcing a fresh connection makes BBB appear
The exploit run proves the proxy only ever authenticated Bearer TOKEN_A; transfer #2's
Proxy-Authorization: Bearer TOKEN_B was dropped because the tunnel was reused, so transfer #2
operated under transfer #1's proxy credential. The control (CURLOPT_FRESH_CONNECT on transfer #2)
shows both credentials reaching the proxy when a new tunnel is forced — isolating the cause to the
reuse decision, not to header emission.
Steps to reproduce
gcc -I include attachments/proxy_client.c lib/.libs/libcurl.a \
-lssl -lcrypto -lbrotlidec -lbrotlicommon -lz -o proxy_client
python3 -I attachments/poc_proxy_hdr_reuse.py # edit SCRATCH paths at top if needed
Suggested fix
Include the CURLOPT_PROXYHEADER list in the proxy reuse comparison (e.g. compare
data->set.proxyheaders in proxy_info_matches() / url_match_proxy_use()), so that transfers
presenting different proxy headers are not merged onto the same tunnel — mirroring how proxy
credentials were added to the key for CVE-2026-3784. At minimum, differing Proxy-Authorization
proxy headers should bar reuse.
Impact
Impact
A transfer that is supposed to present a specific proxy-side identity or credential through
CURLOPT_PROXYHEADER (commonly Proxy-Authorization: Bearer ... or a tenant/routing header) can
instead silently reuse an earlier transfer's CONNECT tunnel that was established under a different
proxy identity. The later transfer's proxy headers are never sent to the proxy.
Concrete consequences in a libcurl-embedding client that shares a connection pool:
- Cross-identity / cross-tenant confusion: per-tenant proxy headers do not isolate tunnels; tenant B's request can ride tenant A's authenticated tunnel.
- Privilege inheritance: if the first tunnel was opened with a privileged
Proxy-Authorization, a subsequent transfer that omits it (or carries a lesser token) reuses the privileged tunnel. - Silent credential non-presentation: a rotated/renewed proxy token in
CURLOPT_PROXYHEADERis not actually presented until a fresh connection is made.
What it is / is not
- It is confirmed live on a default build: with a shared pool and differing proxy headers the proxy sees a single CONNECT (first identity only); forcing a fresh connection restores the second identity, isolating the cause to the reuse key.
- It is the same class as CVE-2026-3784, reached through the
CURLOPT_PROXYHEADERchannel that the fix did not cover (that fix keyed onCurl_credsonly). - It is not a remote-attacker primitive on its own: the differing identities come from the application's own configuration across transfers sharing a pool. The security impact lands when those identities are meant to be isolated (multi-tenant / privilege-separated proxy use).
- It is not memory-unsafe.
Honest severity
Low, argued toward Medium in multi-identity/multi-tenant proxy clients (the parent CVE-2026-3784 is Medium). I rate it Low by default because exploitation requires the application to intentionally use different proxy headers across pooled transfers; I report it because it is a demonstrable, unpatched continuation of a class curl already treats as a security bug, with a clear keying fix.