curl · Report #3697719
CVE-2026-7168: cross-proxy Digest auth state leak
How it was found
Summary:
On libcurl 8.19.0, Proxy Digest state learned from proxyA survives an independent transfer boundary on a reused easy handle and is emitted preemptively to proxyB when the proxy is changed. In the attached C PoC, the first CONNECT to proxyB carries Proxy-Authorization: Digest ... built from proxyArealm / proxyAnonce instead of starting unauthenticated and waiting for a challenge from proxyB. The leaked header is replayable to proxyA for the same CONNECT authority and is accepted with 200, while replay to a different authority is rejected with 407. A fresh easy handle and curl_easy_reset() both suppress the leak. I also confirmed that changing proxy credentials before the second transfer still causes libcurl to build the first proxyB CONNECT from stale proxyA Digest challenge state, and that a proxyB which only advertises Basic still receives stale Digest on the first CONNECT.
Affected version
Reproduced on Linux x86_64 with:
curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 OpenSSL/3.5.5 zlib/1.3.1 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5 libssh2/1.11.1 nghttp2/1.68.1 ngtcp2/1.21.0 nghttp3/1.15.0 librtmp/2.3 mit-krb5/1.22.1 OpenLDAP/2.6.10
The attached C PoC also prints the runtime version string:
curl_version=libcurl/8.19.0 ...
Steps To Reproduce:
- Extract the attached
curl_proxy_connect_bundle.zip. - Start the local proxy harness:
python3 proxy_digest_connect_servers.py proxy_events.jsonl - Compile the attached C PoC against the libcurl 8.19.0 build you want to test. On my system I used:
cc -I/home/nobcoder/curl/curl/include poc_proxy_connect_digest_reuse.c /usr/lib/x86_64-linux-gnu/libcurl.so.4 -o poc_proxy_connect_digest_reuse_c - Run the PoC:
./poc_proxy_connect_digest_reuse_c - Observe the base case output:
base1_res=0 base1_code=200base2_res=56 base2_code=0leaked_proxy_b=Digest username="proxyuser", realm="proxyArealm", nonce="proxyAnonce", uri="victim.test:18099", ...raw_first_connect_proxy_b=CONNECT victim.test:18099 HTTP/1.1 ... Proxy-Authorization: Digest username="proxyuser", realm="proxyArealm", nonce="proxyAnonce", ...replay_status=200replay_used_password=falsemismatch_status=407
- Observe the controls:
- fresh easy handle:
fresh_first_b=(null) curl_easy_reset():reset_first_b=(null)- changed credentials before proxyB:
altcreds_first_b=Digest username="otheruser", realm="proxyArealm", nonce="proxyAnonce", ... - proxyB that only advertises Basic:
basicchallenge_first_b=Digest username="proxyuser", realm="proxyArealm", nonce="proxyAnonce", ...
- fresh easy handle:
- Inspect the attached
proxy_events.jsonlfor the full raw request log from proxyA and proxyB, including the exact first CONNECT to proxyB.
Relevant source lines against release tag curl-8_19_0:
lib/urldata.h:963-966storesproxydigestandauthproxyon easy-handle statelib/transfer.c:500-501and550-551preserve proxy auth state across new transferslib/http_digest.c:93-97,120, and161-162read staledata->state.proxydigestand buildProxy-Authorization: Digest ...lib/http.c:809andlib/http_proxy.c:225-238place proxy auth on the CONNECT pathlib/url.c:588-595and904-919show curl already separates connections by proxy identity, so this is not just wrong cross-proxy connection reuse
Impact
Summary:
This is a cross-proxy authentication boundary break. A later attacker-controlled proxy can receive a Proxy-Authorization: Digest response that belongs to a different proxy’s CONNECT challenge, and in the attached C PoC that leaked header is replayable to proxyA for the same CONNECT authority and is accepted with 200 without the attacker knowing the password. A different CONNECT authority is rejected with 407, which shows the leaked header retains real authentication value rather than being stale metadata only. Fresh easy handles and curl_easy_reset() do not leak the header, which ties the issue to stale easy-handle-scoped Proxy Digest state crossing proxy identity boundaries.