send each credential only to its own hop in write_request (#2579)

* send each credential only to its own hop in write_request

An SSLClient behind a proxy sent Proxy-Authorization inside the TLS tunnel, where the origin reads it, and sent the origin's Authorization on the CONNECT request the proxy reads. Attach each only on the message its hop actually reads.

* Keep default headers off the CONNECT request

set_default_headers() is typically used for origin credentials such as
Authorization, Cookie or API keys, but they were also attached to the
CONNECT request an SSLClient sends to its proxy, in plaintext before the
TLS tunnel exists. Default headers now go only on requests the origin
reads, the same split the previous commit makes for set_basic_auth and
set_bearer_token_auth.

Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi

* Simplify per-hop credential handling and its tests

Flatten the Authorization insertion in write_request into one guard with
an else-if (Basic already took precedence over Bearer), and shorten the
comments around it. Fold DefaultHeadersStayOffConnect into the
CredentialsStayWithTheirHop helper, which now takes the list of headers
that must reach only the origin.

Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
This commit is contained in:
metsw24-max
2026-09-11 13:25:48 -04:00
committed by GitHub
co-authored by yhirose
parent 88956ccad8
commit 515b8f84af
2 changed files with 108 additions and 13 deletions
+17 -13
View File
@@ -14929,8 +14929,12 @@ inline Result ClientImpl::send_(Request &&req) {
inline void ClientImpl::prepare_default_headers(Request &r, bool for_stream,
const std::string &ct) {
(void)for_stream;
for (const auto &header : default_headers_) {
if (!r.has_header(header.first)) { r.headers.insert(header); }
// Default headers are meant for the origin and may carry its credentials, so
// keep them off the CONNECT request the proxy reads.
if (r.method != "CONNECT") {
for (const auto &header : default_headers_) {
if (!r.has_header(header.first)) { r.headers.insert(header); }
}
}
// RFC 9110 5.3 recommends sending control data such as Host first, so
@@ -15611,24 +15615,24 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
}
}
if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
if (!req.has_header("Authorization")) {
// A CONNECT request is read by the proxy; everything sent through the tunnel
// it opens is read by the origin. Each credential goes only to its own hop.
auto is_connect = req.method == "CONNECT";
if (!is_connect && !req.has_header("Authorization")) {
if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
req.headers.insert(make_basic_authentication_header(
basic_auth_username_, basic_auth_password_, false));
}
}
if (!bearer_token_auth_token_.empty()) {
if (!req.has_header("Authorization")) {
} else if (!bearer_token_auth_token_.empty()) {
req.headers.insert(make_bearer_token_authentication_header(
bearer_token_auth_token_, false));
}
}
// Proxy-Authorization is only sent when the proxy is actually used for
// this target — otherwise NO_PROXY-matched requests would leak proxy
// credentials directly to the destination server.
if (is_proxy_enabled_for_host(host_)) {
// Proxy-Authorization is only sent when the proxy reads this message —
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
// would leak proxy credentials to the destination server.
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
if (!proxy_basic_auth_username_.empty() &&
!proxy_basic_auth_password_.empty() &&
!req.has_header("Proxy-Authorization")) {