HexBrain

curl · Report #3707747

libcurl 8.20.0 incomplete fix for CVE-2026-7168: changing only CURLOPT_PROXYPORT leaks stale Proxy Digest auth to a different proxy

Informativestatus
mediumseverity
18upvotes
5 May 2026disclosed

How it was found

Summary:

I found an incomplete-fix variant of CVE-2026-7168 in curl 8.20.0. The 8.20.0 fix clears state.proxydigest / state.authproxy when CURLOPT_PROXY changes, but not when only CURLOPT_PROXYPORT changes. On the same easy handle, request 1 through proxyA (CURLOPT_PROXYPORT=18197) learns Proxy Digest state, and request 2 through proxyB (CURLOPT_PROXYPORT=18198) sends a first CONNECT with stale Proxy-Authorization: Digest ... built from proxyArealm / proxyAnonce. The attached minimal C PoC keeps CURLOPT_PROXY fixed as http://127.0.0.1, changes only CURLOPT_PROXYPORT, and the server output shows proxyB receiving the stale proxyA-derived Digest header on its first CONNECT. A fresh easy handle does not send that header on the first CONNECT to proxyB. This reproduces for me on the clean curl-8_20_0 release tag and also on current master faa4b0692d30986c498d91ad36223cbf02796ad1.

Affected version

Reproduced on Linux x86_64 from a local build of:

  • tag: curl-8_20_0
  • commit: a05f34973e6c4bb629d018f7cb51487be1c904d8
  • runtime string from my local build: libcurl/8.20.0-DEV OpenSSL/3.5.5 zlib/1.3.1

Also reproduced on:

  • branch: master
  • commit: faa4b0692d30986c498d91ad36223cbf02796ad1
  • runtime string: libcurl/8.20.1-DEV OpenSSL/3.5.5 zlib/1.3.1

Steps To Reproduce:

  1. Start the attached minimal proxy server: python3 proxyport_digest_min_server.py

  2. Build the attached minimal client against curl-8_20_0: cc -I/tmp/curl-8_20_0/include poc_proxyport_digest_min.c /tmp/curl-8_20_0/build-audit/lib/libcurl.so -Wl,-rpath,/tmp/curl-8_20_0/build-audit/lib -o poc_proxyport_digest_min_8200

  3. Run it: ./poc_proxyport_digest_min_8200

  4. Observe the client output:

    • req1_res=0
    • req2_res=7
  5. Observe the proxy output:

    • proxyA request 1: no Proxy-Authorization
    • proxyA request 2: Proxy-Authorization: Digest ... realm="proxyArealm", nonce="proxyAnonce", ...
    • proxyB request 1: Proxy-Authorization: Digest ... realm="proxyArealm", nonce="proxyAnonce", ...

This reproducer keeps CURLOPT_PROXY fixed as http://127.0.0.1 and changes only CURLOPT_PROXYPORT from 18197 to 18198.

Relevant source lines in curl-8_20_0:

  • lib/setopt.c:1037 — CURLOPT_PROXYPORT only updates s->proxyport
  • lib/setopt.c:1626 — the 8.20.0 fix clears state.proxydigest / state.authproxy only when the CURLOPT_PROXY string changes
  • lib/url.c:2059 — the effective proxy port comes from data->set.proxyport
  • lib/transfer.c:537 and lib/transfer.c:592 — proxy auth state survives across transfers
  • lib/http_digest.c:93 and lib/http_digest.c:161 — stale Proxy Digest state is reused to build Proxy-Authorization: Digest ...

Impact

This is an incomplete-fix variant of CVE-2026-7168. Changing only CURLOPT_PROXYPORT is enough to move the easy handle to a different proxy identity while preserving stale Proxy Digest state from the first proxy. As a result, a later proxy can receive a Proxy-Authorization: Digest header derived from another proxy’s challenge on the first CONNECT request. This is not expected negotiation state for proxyB; it is stale authenticated state crossing a proxy identity boundary.

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