CVE-2026-19931: Negotiate ambient user conn reuse
What the report claims
curl 8.21.0 on Windows with SSPI/Negotiate incorrectly reuses connections authenticated with ambient credentials for subsequent requests that specify different explicit credentials, causing the later request to be processed under the former identity.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2026-19931 Negotiate ambient user conn reuse
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The code supports the reported flaw. A maintainer should review its impact and decide what to do next.
Why this finding?
- The code at lib/url.c:975-979 shows the credential comparison is guarded by `if(conn->creds &&`, meaning when conn->creds is NULL (as with ambient SSPI credentials), the check is entirely skipped.
- The function already tracks Negotiate authentication state via `conn->http_negotiate_state != GSS_AUTHNONE` (used at lines 981, 1007), which is the correct condition for the fix.
- This allows a connection authenticated as one identity (via ambient credentials) to be reused for a request with different explicit credentials, causing credential confusion - a clear security issue.
- The deterministic checks confirm all cited symbols exist in the codebase at the target revision.
- The issue does not fall under any 'not security issues' category in the project policy.
What still needs checking
- The PoC and server code (poc.c, server.cs) were not provided for verification, but the code flaw is directly visible in the source.
How this analysis ran 5 claims · 13 citations verified
Checked against curl master on 2026-08-07 (a15cf1ea0796). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the detailed report. The flaw is confirmed in lib/url.c:976 where the credential comparison is guarded by `if(conn->creds &&`, skipping the check when ambient credentials leave conn->creds as NULL. The fix using `conn->http_negotiate_state != GSS_AUTHNONE` is correct since that state is already tracked in the same function (lines 981, 1007). This is a valid security issue — credential confusion allowing one user's request to execute as another identity.
Inspect all claims and source code 5 claims · 4 code excerpts
Report claims
The credential comparison in url_match_auth_nego() is skipped when conn->creds is NULL, allowing a connection authenticated with ambient credentials to be reused for a request with different explicit credentials.
Read the reasoning
The code at lib/url.c:975-979 shows the credential/origin comparison is only performed when conn->creds is non-NULL. If ambient SSPI credentials result in conn->creds == NULL, the check is skipped, allowing connection reuse across different identities.
The flaw occurs in the url_match_auth_nego() function where the check for credential equality is guarded by conn->creds != NULL.
Read the reasoning
The function url_match_auth_nego exists at lib/url.c:969 and the specific flawed guard is at line 976.
A connection that was established using ambient SSPI credentials (conn->creds == NULL) is reused for a subsequent request that supplies explicit credentials for a different user.
Read the reasoning
The trigger condition is exactly when conn->creds == NULL (ambient credentials) but the connection has entered Negotiate auth state (http_negotiate_state != GSS_AUTHNONE). The code already has the state tracking needed for a proper fix.
The later request is processed with the identity of the original connection, exposing data to the wrong user or allowing unauthorized access under the original identity.
Read the reasoning
When the credential check is skipped due to conn->creds == NULL, the connection is accepted for reuse (m->found = conn at line 1006), causing the later request to use the original connection's authenticated identity.
Run a local Negotiate server, request A without explicit credentials (authenticates as Administrator), then request B with explicit credentials for userB; on curl 8.21.0 request B reuses the connection and is processed as Administrator.
Read the reasoning
The reproduction files (poc.c, server.cs) are referenced but not included in the provided source code. The deterministic checks confirm the relevant symbols exist, but the actual reproduction cannot be verified from the provided code alone.
What the report says about other projects
not addressed The credential comparison should depend on whether the connection has entered Negotiate authentication state, rather than whether conn->creds is non-NULL.
not addressed The credential comparison should depend on whether the connection has entered Negotiate authentication state, rather than whether conn->creds is non-NULL.
not addressed The credential comparison should depend on whether the connection has entered Negotiate authentication state, rather than whether conn->creds is non-NULL.
Read the original report
Source code
| 969 | static bool url_match_auth_nego(struct connectdata *conn, |
| C2: Function definition: static bool url_match_auth_nego(struct connectdata *conn, struct url_conn_match *m) | |
| 970 | struct url_conn_match *m) |
| 971 | { |
| 972 | /* If we are looking for an HTTP+Negotiate connection, check if this is |
| 973 | already authenticating with the right credentials. If not, keep looking |
| 974 | so that we can reuse Negotiate connections if possible. */ |
| 975 | if(m->want_nego_http) { |
| C1: The credential comparison is guarded by `if(conn->creds &&` at line 976 | |
| C2: The flaw occurs in the if(m->want_nego_http) block starting here | |
| C4: When m->want_nego_http is true and conn->creds is NULL, the function returns TRUE (line 1017) allowing reuse | |
| 976 | if(conn->creds && |
| C1: When conn->creds is NULL, the entire Curl_creds_same and Curl_peer_equal check is skipped | |
| C2: The guard condition `if(conn->creds &&` is the location of the flaw | |
| C3: The condition `if(conn->creds &&` means when conn->creds == NULL, the credential check is bypassed | |
| 977 | (!Curl_creds_same(conn->creds, m->data->state.creds) || |
| C1: Curl_creds_same comparison only runs when conn->creds is non-NULL | |
| 978 | !Curl_peer_equal(conn->creds_origin, m->data->state.origin))) |
| C1: Curl_peer_equal comparison only runs when conn->creds is non-NULL | |
| 979 | return FALSE; |
| 980 | } |
| 981 | else if(conn->http_negotiate_state != GSS_AUTHNONE) { |
| C3: The code already tracks Negotiate auth state via conn->http_negotiate_state != GSS_AUTHNONE | |
| 982 | /* Connection is using Negotiate auth but we do not want Negotiate */ |
| 983 | return FALSE; |
| 984 | } |
| 985 | |
| 986 | #ifndef CURL_DISABLE_PROXY |
| 987 | /* Same for Proxy Negotiate authentication */ |
| 988 | if(m->want_proxy_nego_http) { |
| 989 | /* Both conn->http_proxy.user and conn->http_proxy.passwd can be |
| 990 | * NULL */ |
| 991 | if(!conn->http_proxy.creds) |
| 992 | return FALSE; |
| 993 | |
| 994 | if(!Curl_creds_same(m->needle->http_proxy.creds, conn->http_proxy.creds)) |
| 995 | return FALSE; |
| 996 | } |
| 997 | else if(conn->proxy_negotiate_state != GSS_AUTHNONE) { |
| 998 | /* Proxy connection is using Negotiate auth but we do not want Negotiate */ |
| 999 | return FALSE; |
| 1000 | } |
| 1001 | #endif |
| 1002 | if(m->want_nego_http || m->want_proxy_nego_http) { |
| C4: If want_nego_http is true, m->found = conn is set at line 1006, marking the connection for reuse | |
| 1003 | /* Credentials are already checked, we may use this connection. We MUST |
| 1004 | * use a connection where it has already been fully negotiated. If it has |
| 1005 | * not, we keep on looking for a better one. */ |
| 1006 | m->found = conn; |
| 1007 | if((m->want_nego_http && |
| C3: http_negotiate_state is used elsewhere in the same function to detect Negotiate auth state | |
| 1008 | (conn->http_negotiate_state != GSS_AUTHNONE)) || |
| 1009 | (m->want_proxy_nego_http && |
| 1010 | (conn->proxy_negotiate_state != GSS_AUTHNONE))) { |
| 1011 | /* We must use this connection, no other */ |
| 1012 | m->force_reuse = TRUE; |
| 1013 | return TRUE; |
| 1014 | } |
| 1015 | return FALSE; /* get another */ |
| C4: Returns FALSE only to keep looking for a better connection, but the connection is still marked as found | |
| 1016 | } |
| 1017 | return TRUE; |
| 1018 | } |
| 158 | bool Curl_creds_same(struct Curl_creds *c1, struct Curl_creds *c2) |
| 159 | { |
| 160 | return (c1 == c2) || |
| 161 | (c1 && c2 && |
| 162 | !Curl_timestrcmp(c1->user, c2->user) && |
| 163 | !Curl_timestrcmp(c1->passwd, c2->passwd) && |
| 164 | !Curl_timestrcmp(c1->oauth_bearer, c2->oauth_bearer) && |
| 165 | !Curl_timestrcmp(c1->sasl_authzid, c2->sasl_authzid) && |
| 166 | !Curl_timestrcmp(c1->sasl_service, c2->sasl_service)); |
| 167 | } |
Show 7 lines (not cited by the judge)
| 321 | bool Curl_peer_equal(struct Curl_peer *p1, struct Curl_peer *p2) |
| 322 | { |
| 323 | return (p1 == p2) || |
| 324 | (p1 && p2 && |
| 325 | (p1->scheme == p2->scheme) && |
| 326 | Curl_peer_same_destination(p1, p2)); |
| 327 | } |
Show 1 lines (not cited by the judge)
| 833 | #define CURLAUTH_NEGOTIATE (((unsigned long)1) << 2) |