HexBrain

curl · Report #3972094

CVE-2026-87861: cross-origin reuse of a Negotiate connection

Validstatus
lowseverity
1upvotes
14 Oct 2026disclosed

How it was found

Summary:

I found a cross-origin connection reuse flaw in lib/url.c, introduced by bd10924b4 as part of the recently announced "Patch the Planet" initiative. It causes a request for one origin to be sent on a connection that was authenticated with Negotiate for a different origin, while the requesting transfer's own credentials go unused.

My research looked at the reuse matchers that bd10924b4 rewrote. The commit closed #22040 by adding conn->creds_origin and comparing it against data->state.origin. This activity is correct, but the issue is in the branch the commit replaced. At the parent c2b050e4e496, the credential mismatch branch of url_match_auth_ntlm recorded a connection as a reuse candidate only when both auth states were idle:

#ifdef USE_SPNEGO
      if((conn->http_ntlm_state == NTLMSTATE_NONE) &&
         (conn->http_negotiate_state == GSS_AUTHNONE)) {
#else
      if(conn->http_ntlm_state == NTLMSTATE_NONE) {
#endif
        m->found = conn;
      }

bd10924b4 replaced that entire branch with a single early return gated on conn->creds, so the GSS_AUTHNONE condition is gone. That gate does not fire in the ordinary Kerberos SSO case. Curl_http_output_auth exempts Negotiate from the no-credentials early out, so --negotiate with no -u runs from the ticket cache with data->state.creds NULL, and Curl_input_negotiate links conn->creds_origin to the origin while Curl_creds_link(&conn->creds, NULL) leaves conn->creds NULL. A later transfer whose wanted auth includes NTLM (CURLAUTH_ANY masks off only CURLAUTH_DIGEST_IE and CURLAUTH_HTTPSIG, so --anyauth qualifies) then passes url_match_auth, because needle->creds is NULL for HTTP and Curl_creds_same(NULL, NULL) is TRUE.

It reaches the state-NONE branch at url.c:937, where the origin comparison is gated on conn->creds and does not run. Control falls through to url.c:962, which sets m->found = conn and returns FALSE because the NTLM state is idle. url_match_conn returns FALSE on that, so url_match_auth_nego, the only matcher that compares the two origins, never runs, and url_match_result attaches match->found when no better candidate was found. Both origins need to sit behind one forwarding proxy, which lib/proxy.c arranges by setting conn->origin to the proxy peer, putting every origin behind that proxy in a single cpool bundle.

Affected version

curl 8.21.0-DEV (Darwin) libcurl/8.21.0-DEV OpenSSL/3.6.3 mit-krb5/1.7-prerelease OpenLDAP/2.4.28/Apple Release-Date: [unreleased] Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt mqtts pop3 pop3s rtsp smtp smtps telnet tftp ws wss Features: alt-svc AsynchDNS GSS-API HSTS HTTPS-proxy IPv6 Kerberos Largefile NTLM SPNEGO SSL threadsafe TLS-SRP UnixSockets

Steps To Reproduce:

Requires git, cmake, a C compiler, OpenSSL, MIT Kerberos and uv.

  1. Build curl at the affected commit.
git clone https://github.com/curl/curl.git curl-src
cd curl-src && git checkout bd10924b47c8bf94127eaee8c56e7b5e5418da46
cmake -S . -B bld \
  -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_CURL_EXE=ON \
  -DCURL_USE_OPENSSL=ON -DCURL_USE_GSSAPI=ON -DCURL_ENABLE_NTLM=ON \
  -DCURL_USE_LIBPSL=OFF -DUSE_LIBIDN2=OFF -DUSE_NGHTTP2=OFF \
  -DCURL_ZLIB=OFF -DCURL_BROTLI=OFF -DCURL_ZSTD=OFF -DENABLE_CURL_MANUAL=OFF
cmake --build bld -j8 --target curl

./bld/src/curl -V must list SPNEGO and NTLM in Features.
  1. Unpack the archive and point it at that binary.
unzip curl-22040-ntlm-connection-reuse.zip
cd curl-22040-ntlm-connection-reuse/pocs/ntlm-cross-origin-reuse
export CURL_BIN=/path/to/curl-src/bld/src/curl
./run.sh

Result at bd10924b4:

  conn  origin        Authorization       status
  1     a.internal    NTLM type1          401
  1     b.internal    NTLM type3          200

RESULT: REUSED ACROSS ORIGINS

One connection sends the type1 for a.internal, takes that origin's challenge, then sends the type3 on a request for b.internal.

Impact

Summary:

A request for one origin is sent on a connection that was already authenticated with Negotiate for a different origin, so the second host sees the first host's authenticated session and the requesting transfer's own credentials go unused. Both origins have to sit behind one forwarding proxy, which is what places them in a single connection bundle, so the realistic setting is an application doing Kerberos SSO across internal hosts through a corporate proxy. The result is that a request meant for host B is attributed to the caller's identity on host A, with no credential check having run on the reuse path.

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