Summary
When set_proxy_digest_auth() is configured and an HTTPS request goes through a proxy (CONNECT tunnel), an origin server that returns 407 Proxy-Authentication-Required with a Digest challenge can extract the user's proxy credentials.
This is not related to PR #2448 — that PR only fixed the NO_PROXY-bypassed variant (see commit 9f7eea6). The CONNECT-tunnel variant is pre-existing and survives that fix.
Reproduction
```cpp
httplib::SSLClient cli("origin.example", 443);
cli.set_proxy("proxy.corp", 3128);
cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
cli.Get("/x");
// origin.example replies inside the tunnel with:
// HTTP/1.1 407 Proxy-Authentication-Required
// Proxy-Authenticate: Digest realm="x", nonce="...", algorithm=MD5
//
// httplib computes a Proxy-Authorization header from proxy-user/proxy-pass
// and re-sends the request inside the same CONNECT tunnel — straight to
// origin.example. origin.example now has:
// - username (plaintext)
// - digest response: MD5(MD5("proxy-user:x:proxy-pass") : nonce : MD5("GET:/x"))
// which is the full input for an offline dictionary attack on proxy-pass.
```
Root cause
`httplib.h` around line 13195 (the 401/407 retry block under `#ifdef CPPHTTPLIB_SSL_ENABLED`) dispatches on response status alone:
```cpp
auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407;
// (PR #2448 added) if (is_proxy && !is_proxy_enabled_for_host(host_)) { return ret; }
const auto &username = is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;
const auto &password = is_proxy ? proxy_digest_auth_password_ : digest_auth_password_;
```
`is_proxy_enabled_for_host(host_)` returns true for the in-tunnel case (proxy is configured, host is not in NO_PROXY), so the new guard does not fire. Response status is attacker-controlled, so origin can extract proxy creds simply by returning 407.
Why other libraries don't have this
curl, Python `requests`, Go `net/http`, JDK `HttpClient` either don't auto-retry on 401/407, or they pair the status with connection context (was this response received from the proxy hop or from the origin via tunnel?) before deciding which credential bucket to use.
Suggested fix
Track whether the current `Stream`/connection represents an established CONNECT tunnel to the origin (vs. a plain proxy connection). When an inner-tunnel response returns 407, do not enter the proxy-digest retry path — propagate the 407 to the caller.
Rough shape:
```cpp
if (is_proxy && /* response came from inside an established CONNECT tunnel */) {
return ret;
}
```
Severity
- Requires `set_proxy_digest_auth()` configured
- Requires a malicious / misconfigured origin that returns 407 with a Digest challenge
- Leaks: username (plaintext) + digest response (enables offline attack on the password)
- Encrypted in transit (TLS), but visible to origin itself
Not a zero-day, but the proxy credentials are exactly the kind of long-lived secret users don't expect to leak to arbitrary upstream servers.
Scope
Pre-existing. Surfaced during code review of #2448 but explicitly out of that PR's scope.
Summary
When
set_proxy_digest_auth()is configured and an HTTPS request goes through a proxy (CONNECT tunnel), an origin server that returns407 Proxy-Authentication-Requiredwith aDigestchallenge can extract the user's proxy credentials.This is not related to PR #2448 — that PR only fixed the
NO_PROXY-bypassed variant (see commit 9f7eea6). The CONNECT-tunnel variant is pre-existing and survives that fix.Reproduction
```cpp
httplib::SSLClient cli("origin.example", 443);
cli.set_proxy("proxy.corp", 3128);
cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
cli.Get("/x");
// origin.example replies inside the tunnel with:
// HTTP/1.1 407 Proxy-Authentication-Required
// Proxy-Authenticate: Digest realm="x", nonce="...", algorithm=MD5
//
// httplib computes a Proxy-Authorization header from proxy-user/proxy-pass
// and re-sends the request inside the same CONNECT tunnel — straight to
// origin.example. origin.example now has:
// - username (plaintext)
// - digest response: MD5(MD5("proxy-user:x:proxy-pass") : nonce : MD5("GET:/x"))
// which is the full input for an offline dictionary attack on proxy-pass.
```
Root cause
`httplib.h` around line 13195 (the 401/407 retry block under `#ifdef CPPHTTPLIB_SSL_ENABLED`) dispatches on response status alone:
```cpp
auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407;
// (PR #2448 added) if (is_proxy && !is_proxy_enabled_for_host(host_)) { return ret; }
const auto &username = is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;
const auto &password = is_proxy ? proxy_digest_auth_password_ : digest_auth_password_;
```
`is_proxy_enabled_for_host(host_)` returns true for the in-tunnel case (proxy is configured, host is not in NO_PROXY), so the new guard does not fire. Response status is attacker-controlled, so origin can extract proxy creds simply by returning 407.
Why other libraries don't have this
curl, Python `requests`, Go `net/http`, JDK `HttpClient` either don't auto-retry on 401/407, or they pair the status with connection context (was this response received from the proxy hop or from the origin via tunnel?) before deciding which credential bucket to use.
Suggested fix
Track whether the current `Stream`/connection represents an established CONNECT tunnel to the origin (vs. a plain proxy connection). When an inner-tunnel response returns 407, do not enter the proxy-digest retry path — propagate the 407 to the caller.
Rough shape:
```cpp
if (is_proxy && /* response came from inside an established CONNECT tunnel */) {
return ret;
}
```
Severity
Not a zero-day, but the proxy credentials are exactly the kind of long-lived secret users don't expect to leak to arbitrary upstream servers.
Scope
Pre-existing. Surfaced during code review of #2448 but explicitly out of that PR's scope.