HexBrain

curl · Report #3750295

CVE-2026-9079: stale proxy password leak

Validstatus
mediumseverity
4upvotes
24 Jun 2026disclosed

How it was found

Product

Product name: curl / libcurl

Product link: https://github.com/curl/curl

Suggested CWE: CWE-226: Sensitive Information in Resource Not Removed Before Reuse (https://cwe.mitre.org/data/definitions/226.html); alternative CWE-200: Exposure of Sensitive Information to an Unauthorized Actor (https://cwe.mitre.org/data/definitions/200.html)

Affected versions: 8.8.0 <= libcurl <= 8.20.0 are confirmed affected. curl/libcurl 8.21.0-DEV, commit b2476a07128fc1e83a0b322fe6eb9dfa761db53d, is also affected.

Unaffected versions: libcurl <= 8.7.1 do not contain the CURLOPT_PROXYUSERPWD setter change that introduced this issue.

Introduced in: this issue was introduced by commit d5e83eb745762f48d8fafadc5df5dd3ae8d8941e (url: do not URL decode proxy credentials) in curl 8.8.0. This commit changed CURLOPT_PROXYUSERPWD from writing directly to the internal proxy username/password fields to first parsing the input into temporary u / p variables and then writing back only the components that exist.

Reference: https://github.com/curl/curl/commit/d5e83eb745762f48d8fafadc5df5dd3ae8d8941e

Summary

curl is a widely used command-line network transfer tool, and libcurl is the transfer library it provides for integration into other applications. libcurl provides the CURLOPT_PROXYUSERPWD option to set the username:password used for HTTP proxy authentication. The official documentation states that when this option is set multiple times, the last set string overrides the previous one, and setting this option to NULL disables its use.

However, the implementation of CURLOPT_PROXYUSERPWD has a stale-state issue. When the same easy handle is first configured with proxy credentials such as victim:secret, and later CURLOPT_PROXYUSERPWD is set to a username-only value such as attacker, or set to NULL in an attempt to clear the credentials, the old proxy password secret remains stored inside the handle. When a later request is sent through a proxy and proxy authentication is triggered, libcurl sends the old password to the proxy server, leaking a previous request's or previous task's proxy password to a later proxy.

This is a credential disclosure vulnerability. It primarily affects applications that reuse libcurl easy handles and allow tasks with different trust levels to configure proxy parameters, such as proxy pools, download services, crawler platforms, CI/automation systems, and other multi-tenant services built on libcurl.

Details

The official documentation describes the semantics of CURLOPT_PROXYUSERPWD as follows:

  • This option sets the [username]:[password] to use for the connection to the HTTP proxy.
  • When this option is set multiple times, the last set string overrides the previous ones.
  • Setting this option to NULL disables its use.

Reference: https://curl.se/libcurl/c/CURLOPT_PROXYUSERPWD.html

The issue is in the setter logic for CURLOPT_PROXYUSERPWD. The option first parses the provided username:password string into temporary variables u and p, then URL-decodes those components and writes them to the internal proxy username and proxy password fields. The implementation replaces the internal username only when u is non-null, and replaces the internal password only when p is non-null:

case CURLOPT_PROXYUSERPWD: {
  char *u = NULL;
  char *p = NULL;
  result = setstropt_userpwd(ptr, &u, &p);

  if(!result && u) {
    curlx_safefree(s->str[STRING_PROXYUSERNAME]);
    result = Curl_urldecode(u, 0, &s->str[STRING_PROXYUSERNAME], NULL,
                            REJECT_ZERO);
  }
  if(!result && p) {
    curlx_safefree(s->str[STRING_PROXYPASSWORD]);
    result = Curl_urldecode(p, 0, &s->str[STRING_PROXYPASSWORD], NULL,
                            REJECT_ZERO);
  }

As a result:

  1. After setting CURLOPT_PROXYUSERPWD = "victim:secret", the internal proxy username is victim and the internal proxy password is secret.
  2. When CURLOPT_PROXYUSERPWD = "attacker" is set next, parsing produces only the username attacker and no password component. The implementation replaces the username but does not clear the old password, so the internal state becomes attacker:secret.
  3. When CURLOPT_PROXYUSERPWD = NULL is set next, parsing produces neither a username nor a password component. The implementation does not free the old username or old password, so attacker:secret remains stored.
  4. Later proxy authentication uses the internal proxy credentials to generate a Proxy-Authorization header, causing the old password to leak across logical requests to a later proxy.

This behavior violates the documented override and clear semantics. It is not a result of an application using an undocumented API. It occurs when using the documented repeated-set and NULL clear semantics, because libcurl fails to correctly clear sensitive state.

Affected versions: 8.8.0 <= libcurl <= 8.20.0 are confirmed affected. libcurl <= 8.7.1 does not contain the setter change that introduced this issue. This range is based on the version history of the CURLOPT_PROXYUSERPWD setter in lib/setopt.c: 8.7.1 still writes directly through setstropt_userpwd() and clears the internal STRING_PROXYUSERNAME / STRING_PROXYPASSWORD fields; commit d5e83eb745762f48d8fafadc5df5dd3ae8d8941e in 8.8.0 (url: do not URL decode proxy credentials, https://github.com/curl/curl/commit/d5e83eb745762f48d8fafadc5df5dd3ae8d8941e) changed CURLOPT_PROXYUSERPWD to first parse into temporary u / p variables and then write back only the components that exist, introducing stale username/password state. This issue has been verified on Alpine 3.20's default distribution package libcurl 8.14.1-r2, and also on curl/libcurl 8.21.0-DEV, commit b2476a07128fc1e83a0b322fe6eb9dfa761db53d. If maintainers release additional versions before the fix, the affected range should extend up to the first fixed version.

PoC

The PoC is located in the poc/ directory beside this report. The default PoC uses Alpine 3.20's distribution-provided libcurl 8.14.1-r2 and does not compile curl from source. During docker build, it only installs the distribution curl-dev package and compiles a small C reproducer. During docker run, it only starts a local capture proxy and runs the already-built PoC binary to verify the actual Proxy-Authorization value received by the proxy.

PoC files:

  • poc/Dockerfile: builds the reproduction environment, uses Alpine 3.20's default libcurl 8.14.1-r2, and compiles the PoC binary during the build stage.
  • poc/run-built-poc.sh: runs the PoC binary that was compiled during the build stage and checks the output.
  • poc/poc-proxyuserpwd-stale.c: triggers the stale state using libcurl's public API.
  • poc/proxy-capture-server.py: local HTTP proxy that captures and decodes Proxy-Authorization.

Steps to reproduce:

  1. Build the Docker image:

    cd poc
    docker build -t curl-proxyuserpwd-stale-poc .
    
  2. Run the PoC. This step does not compile curl and does not compile the PoC; it only starts the capture proxy and runs the already-built PoC binary:

    docker run --rm curl-proxyuserpwd-stale-poc
    
  3. Expected vulnerable output:

    [poc] transfer return codes:
    SUMMARY initial=0 user_only=0 null_clear=0
    [poc] proxy capture:
    GET http://example.invalid/ auth=Basic dmljdGltOnNlY3JldA== decoded=victim:secret
    GET http://example.invalid/ auth=Basic YXR0YWNrZXI6c2VjcmV0 decoded=attacker:secret
    GET http://example.invalid/ auth=Basic YXR0YWNrZXI6c2VjcmV0 decoded=attacker:secret
    [poc] VULNERABLE: stale proxy password was sent after replacement/clear
    

The second and third decoded=attacker:secret lines are the evidence of the vulnerability:

  • In the second request, the application has set CURLOPT_PROXYUSERPWD to attacker, so the old password secret should not appear.
  • In the third request, the application has set CURLOPT_PROXYUSERPWD to NULL, so no proxy credentials should be sent.

In a fixed version, the second request should not include the old password, and the third request should not send Proxy-Authorization.

Reporter Severity Estimate

The curl project normally uses Low / Medium / High / Critical for vulnerability severity rather than CVSS. We suggest treating this issue as High. As a reporter-provided reference estimate, the CVSS v4.0 score is 8.2 with vector CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N, for the following reasons:

  • AV:N: the leaked credential is exposed through a network proxy request, and an attacker can obtain it by controlling or observing the later proxy.
  • AC:L: once the application reuses an easy handle and allows later proxy configuration changes, triggering the issue is direct and does not rely on complex races or unstable conditions.
  • AT:P: the target application must reuse an easy handle that previously stored proxy credentials, and later proxy configuration or credentials must be influenced by external input.
  • PR:N: in the baseline scenario, the attacker does not need an account on the target system; the attacker only needs to submit input that affects later proxy configuration in the vulnerable application.
  • UI:N: once the application executes these libcurl API calls, no additional user interaction is required.
  • VC:H: the old proxy password is sent to the later proxy. With Basic authentication, the plaintext username:password can be recovered directly, making this a high-sensitivity credential disclosure.
  • VI:N: the PoC does not demonstrate data modification against the affected system itself.
  • VA:N: the PoC does not demonstrate denial-of-service impact.
  • SC:N/SI:N/SA:N: later misuse of the credential may cause broader business impact, but the direct impact of this vulnerability is proxy credential disclosure within the affected system using libcurl; secondary impacts on other systems' confidentiality, integrity, or availability are not counted here.

Credit

This vulnerability was discovered by:

CVE and credit are preferred.

If you have any questions regarding the vulnerability details, please feel free to reach out to us for further discussion. Our email address is [email protected].

Impact

An attacker can obtain a proxy password previously stored in the same libcurl easy handle. For Basic proxy authentication, the proxy can directly Base64-decode Proxy-Authorization: Basic ... to recover username:password. For other challenge-based proxy authentication schemes, the stale password material may still participate in generating authentication responses, causing authentication state to leak across proxies or tasks.

A typical attack scenario is:

  1. A service uses libcurl and reuses easy handles for performance.
  2. High-trust task A accesses the network through an enterprise proxy and sets CURLOPT_PROXYUSERPWD = "victim:secret".
  3. Low-trust task B reuses the same handle and is allowed to specify a proxy address or proxy credentials.
  4. Task B sets CURLOPT_PROXYUSERPWD to a username-only value, or sets it to NULL in an attempt to clear proxy credentials.
  5. libcurl incorrectly retains the old password and sends Proxy-Authorization: Basic attacker:secret in B's proxy request.
  6. A proxy controlled or observable by the attacker obtains task A's proxy password.

This vulnerability does not affect every one-shot use of the curl command-line tool. It primarily affects applications that use libcurl as a library, reuse easy handles, and change proxy configuration across different trust contexts. This usage pattern is realistic in download services, crawler platforms, proxy pools, automation task systems, and CI/build systems.

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