CVE-2026-9079: stale proxy password leak
What the report claims
curl's CURLOPT_PROXYUSERPWD setter fails to clear stale proxy password when overwritten or cleared, leading to credential leakage across requests.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2026-9079 stale proxy password leak
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The available evidence does not settle the report. A maintainer should investigate.
Why this finding?
- The report is detailed and plausible with a working PoC demonstrating the vulnerability on affected versions.
- Deterministic checks confirm relevant symbols (CURLOPT_PROXYUSERPWD, STRING_PROXYUSERNAME, STRING_PROXYPASSWORD, setstropt_userpwd) exist in the codebase at the checked revision.
- However, the critical CURLOPT_PROXYUSERPWD case handler code in lib/setopt.c is not included in the provided source snippets (only the setstropt_userpwd helper is shown).
- Without the actual setter implementation, the core claims about partial overwrite and missing NULL clearing cannot be verified against the code.
- A human reviewer should examine the CURLOPT_PROXYUSERPWD case in lib/setopt.c at the relevant revision to confirm the reported logic matches the quoted snippet.
What still needs checking
- The CURLOPT_PROXYUSERPWD case handler code from lib/setopt.c (the switch case that calls setstropt_userpwd and conditionally updates STRING_PROXYUSERNAME/STRING_PROXYPASSWORD)
How this analysis ran 6 claims · 0 citations verified
Checked against curl master on 2026-05-20 (edfc80c7c473). 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 and PoC. The issue is plausible and the PoC demonstrates a real credential leak. However, the provided source snippets do not include the CURLOPT_PROXYUSERPWD case handler in lib/setopt.c (only the setstropt_userpwd helper is shown). Could you confirm the exact lines of that case handler in the affected versions, or should we fetch the full lib/setopt.c from the repository to verify the setter logic against your quoted snippet?
Inspect all claims and source code 6 claims · 3 code excerpts
Report claims
When CURLOPT_PROXYUSERPWD is set to a username-only value, the old password is not cleared and remains stored.
Read the reasoning
The claim describes the CURLOPT_PROXYUSERPWD setter logic when a username-only value is provided. The actual case handler code for CURLOPT_PROXYUSERPWD in lib/setopt.c is not included in the provided source snippets (only setstropt_userpwd helper function is shown at lines 132-161). Without the setter code, I cannot verify whether the old password is cleared or retained.
When CURLOPT_PROXYUSERPWD is set to NULL, the old username and password are not freed, leaving stale credentials.
Read the reasoning
The claim describes behavior when CURLOPT_PROXYUSERPWD is set to NULL. The setter code that would handle the NULL case and free STRING_PROXYUSERNAME/STRING_PROXYPASSWORD is not in the provided source. The provided snippets only show curlx_safefree macro definition and setstropt_userpwd, not the CURLOPT_PROXYUSERPWD case logic.
The setter only replaces internal fields when parsed components are non-null, causing partial overwrites.
Read the reasoning
The claim references variables u, p and the string fields, but the actual setter implementation that conditionally writes only non-null components is not in the provided source. The quoted code in the report cannot be verified against the repository at the checked revision.
The documented behavior that setting the option to NULL disables its use is violated by the implementation.
Read the reasoning
The claim compares documented behavior to implementation. The external source E2 mentions historical NULL-clearing behavior for CURLOPT_USERPWD but does not address current CURLOPT_PROXYUSERPWD documentation. The current documentation at https://curl.se/libcurl/c/CURLOPT_PROXYUSERPWD.html was not retrieved. The implementation code is also not provided.
The implementation parses the input into temporary variables u and p before writing back only existing components.
Read the reasoning
The claim describes the parsing approach using temporary u/p variables. This logic resides in the CURLOPT_PROXYUSERPWD case handler, which is not in the provided source. setstropt_userpwd is shown but it is a helper; the caller's use of its output is not visible.
Stale proxy password is sent in Proxy-Authorization header after being overwritten or cleared.
Read the reasoning
The claim describes the runtime effect (stale password sent in Proxy-Authorization). The code_refs are symbol names only; the actual authentication header generation code that reads s->str[STRING_PROXYUSERNAME] and s->str[STRING_PROXYPASSWORD] is not provided.
What the report says about other projects
not addressed When this option is set multiple times, the last set string overrides the previous ones.
not addressed Setting this option to NULL disables its use.
Read the original report (excerpt)
Source code
| 132 | static CURLcode setstropt_userpwd(const char *option, char **userp, |
| 133 | char **passwdp) |
| 134 | { |
| 135 | char *user = NULL; |
| 136 | char *passwd = NULL; |
| 137 | |
| 138 | DEBUGASSERT(userp); |
| 139 | DEBUGASSERT(passwdp); |
| 140 | |
| 141 | /* Parse the login details if specified. If not, then we treat NULL as a |
| 142 | hint to clear the existing data */ |
| 143 | if(option) { |
| 144 | size_t len = strlen(option); |
| 145 | CURLcode result; |
| 146 | if(len > CURL_MAX_INPUT_LENGTH) |
| 147 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 148 | |
| 149 | result = Curl_parse_login_details(option, len, &user, &passwd, NULL); |
| 150 | if(result) |
| 151 | return result; |
| 152 | } |
| 153 | |
| 154 | curlx_free(*userp); |
| 155 | *userp = user; |
| 156 | |
| 157 | curlx_free(*passwdp); |
| 158 | *passwdp = passwd; |
| 159 | |
| 160 | return CURLE_OK; |
| 161 | } |
| 105 | CURLcode Curl_urldecode(const char *string, size_t length, |
| 106 | char **ostring, size_t *olen, |
| 107 | enum urlreject ctrl) |
| 108 | { |
| 109 | size_t alloc; |
| 110 | char *ns; |
| 111 | |
| 112 | DEBUGASSERT(string); |
| 113 | DEBUGASSERT(ctrl >= REJECT_NADA); /* crash on TRUE/FALSE */ |
| 114 | |
| 115 | alloc = (length ? length : strlen(string)); |
| 116 | ns = curlx_malloc(alloc + 1); |
| 117 | |
| 118 | if(!ns) |
| 119 | return CURLE_OUT_OF_MEMORY; |
| 120 | |
| 121 | /* store output string */ |
| 122 | *ostring = ns; |
| 123 | |
| 124 | while(alloc) { |
| 125 | unsigned char in = (unsigned char)*string; |
| 126 | if(('%' == in) && (alloc > 2) && |
| 127 | ISXDIGIT(string[1]) && ISXDIGIT(string[2])) { |
| 128 | /* this is two hexadecimal digits following a '%' */ |
| 129 | in = (unsigned char)((curlx_hexval(string[1]) << 4) | |
| 130 | curlx_hexval(string[2])); |
| 131 | string += 3; |
| 132 | alloc -= 3; |
| 133 | } |
| 134 | else { |
| 135 | string++; |
| 136 | alloc--; |
| 137 | } |
| 138 | |
| 139 | if(((ctrl == REJECT_CTRL) && (in < 0x20)) || |
| 140 | ((ctrl == REJECT_ZERO) && (in == 0))) { |
| 141 | curlx_safefree(*ostring); |
| 142 | return CURLE_URL_MALFORMAT; |
| 143 | } |
| 144 | |
| 145 | *ns++ = (char)in; |
| 146 | } |
| 147 | *ns = 0; /* terminate it */ |
| 148 | |
| 149 | if(olen) |
| 150 | /* store output size */ |
| 151 | *olen = ns - *ostring; |
| 152 | |
| 153 | return CURLE_OK; |
| 154 | } |
Show 5 lines (not cited by the judge)
| 1326 | #define curlx_safefree(ptr) \ |
| 1327 | do { \ |
| 1328 | curlx_free(ptr); \ |
| 1329 | (ptr) = NULL; \ |
| 1330 | } while(0) |