HexBrain

curl · Report #3774977

libcurl 8.20.0 ignores HTTP Digest domain protection space and preemptively leaks Digest auth outside the declared scope

Informativestatus
mediumseverity
6upvotes
5 Jun 2026disclosed

How it was found

Summary:

libcurl 8.20.0 ignores the server-declared HTTP Digest domain protection space for origin authentication and reuses stored Digest state too broadly on the same easy handle.

After a successful Digest-authenticated request, a later request on the same easy handle can receive a preemptive Authorization: Digest ... header even when the new request is outside the domain scope declared by the server's original WWW-Authenticate challenge.

In my strongest reproduction:

  • request 1 receives WWW-Authenticate: Digest realm="realmA", nonce="nonceA", algorithm=MD5, domain="/first"
  • request 2 on the same easy handle to /reuse2 sends a preemptive Authorization: Digest ...
  • a fresh easy handle to /fresh2 sends no Authorization at all
  • forcing a fresh TCP connection for request 2 still leaks the Digest header

This is a Digest protection-space violation with concrete confidentiality impact: Digest auth transcripts are disclosed outside the declared scope and can be used for offline password guessing against low-entropy passwords.

Affected version

Primary reproduced target: curl/libcurl 8.20.0 on Linux x86_64.

I reproduced this using a local build from the official curl 8.20.0 release source.

Representative runtime output from the reproduced build: curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.1 zlib/1.3.1 Release-Date: 2026-04-29 Protocols: http https ipfs ipns ws wss Features: alt-svc AsynchDNS HSTS HTTPS-proxy IPv6 Largefile libz SSL threadsafe TLS-SRP UnixSockets

I also reproduced the same sink on current master at: 24874a4f04cbbafd878441ccef11e3caa5897db5

Steps To Reproduce:

  1. Build curl 8.20.0 locally and build the attached PoC against the resulting libcurl.so. cd /tmp git clone https://github.com/curl/curl.git curl-8.20.0-wt cd /tmp/curl-8.20.0-wt git checkout curl-8_20_0 autoreconf -fi

mkdir -p /tmp/curl-8.20.0-build cd /tmp/curl-8.20.0-build /tmp/curl-8.20.0-wt/configure
--with-openssl
--without-libidn2
--without-libpsl
--without-brotli
--without-zstd
--without-libssh2
--without-nghttp2
--without-libgsasl
--without-gssapi
--disable-ldap
--disable-ldaps
--disable-ftp
--disable-file
--disable-tftp
--disable-rtsp
--disable-dict
--disable-telnet
--disable-pop3
--disable-imap
--disable-smb
--disable-smtp
--disable-gopher
--disable-mqtt
--disable-manual make -j2

cc -I/tmp/curl-8.20.0-wt/include
digest_reuse_probe.c
/tmp/curl-8.20.0-build/lib/.libs/libcurl.so
-Wl,-rpath,/tmp/curl-8.20.0-build/lib/.libs
-o /tmp/digest_reuse_probe_8200

  1. Start the attached plan-driven Digest challenge server with the attached domain-scope plan. python3 digest_challenge_server.py
    --plan digest_plan_domain_scope.json
    --log /tmp/digest_domain_scope.log

The attached plan serves: /first: first response 401 with WWW-Authenticate: Digest realm="realmA", nonce="nonceA", algorithm=MD5, domain="/first", then 200 /reuse2: 200 without ever challenging /fresh2: 200 without ever challenging

  1. Run the PoC against the local build. /tmp/digest_reuse_probe_8200
    alice:sekret
    http://127.0.0.1:18111/first
    http://127.0.0.1:18111/reuse2
    http://127.0.0.1:18111/fresh2

Expected client output: same_1 res=0 code=200 same_2 res=0 code=200 fresh_2 res=0 code=200

  1. Check the server-side log. sed -n '1,20p' /tmp/digest_domain_scope.log

Representative result: {"authorization": null, "path": "/first", "reqno": 1} {"authorization": "Digest username="alice", realm="realmA", nonce="nonceA", uri="/first", response="faf2faf3d759b97cd2d6bcf94dda7cbb", algorithm=MD5", "path": "/first", "reqno": 2} {"authorization": "Digest username="alice", realm="realmA", nonce="nonceA", uri="/reuse2", response="1114f220dfe6109d1466c303466aff70", algorithm=MD5", "path": "/reuse2", "reqno": 3} {"authorization": null, "path": "/fresh2", "reqno": 4}

This shows: the first request negotiates Digest for /first a later request on the same easy handle preemptively sends Digest auth to /reuse2, outside domain="/first" a fresh easy handle to /fresh2 sends no Digest header

  1. Run the fresh-connection control to rule out keep-alive artifacts. DIGEST_FRESH_SECOND=1
    /tmp/digest_reuse_probe_8200
    alice:sekret
    http://127.0.0.1:18111/first
    http://127.0.0.1:18111/reuse2
    http://127.0.0.1:18111/fresh2

The same preemptive Digest header still appears on /reuse2, showing this is reused Digest easy-handle state, not keep-alive connection reuse.

Supporting Material/References:

attached bundle: curl_digest_domain_scope_bundle_2026-06-01.zip included client PoC: digest_reuse_probe.c included plan-driven server: digest_challenge_server.py included strongest plan: digest_plan_domain_scope.json included logs: digest_domain_scope.log digest_domain_scope_master.log digest_domain_scope_master_freshconn.log included write-up: REPORT_digest_domain_scope_preemptive_leak.md

Root cause notes: the preemptive Digest send path in lib/http_digest.c reuses stored Digest state on the easy handle lib/vauth/digest.c parses and stores fields such as nonce, realm, opaque, qop, and algorithm, but does not parse or enforce the domain directive as a result, the server-declared Digest protection space is effectively widened to the full origin for later preemptive reuse on the same easy handle

Impact

In the reproduced flow, a path outside domain="/first" receives:

  • username
  • realm
  • nonce
  • uri
  • response

from a preemptively forwarded Digest header that should not have been sent there.

This leaked Digest transcript can be used for offline password guessing in realistic low-entropy-password deployments. I verified that the captured transcript in the attached PoC is sufficient to recover the sample password sekret from a small dictionary.

This is not an SSRF claim and not a keep-alive reuse artifact. The security claim is limited to preemptive Digest auth disclosure outside the server-declared protection space.

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