ProxyBasicAuthHandler may send HTTPS-scoped credentials to a direct HTTP origin after a 407 response
Summary
When an application explicitly installs ProxyBasicAuthHandler with an HTTPPasswordMgr containing credentials scoped to https://HOST/, a direct (non-proxied) HTTP server at the same authority can return 407 Proxy Authentication Required with the matching realm. ProxyBasicAuthHandler.http_error_407() looks up credentials using req.host, which is scheme-less. HTTPPasswordMgr intentionally allows a scheme-less lookup to match credentials stored for any scheme, so the handler retries the direct HTTP request with a Proxy-Authorization: Basic ... header. The direct origin receives the HTTPS-scoped password in cleartext.
This requires a non-default opener configuration (the application must register ProxyBasicAuthHandler), a credential entry for the same authority at the root path, and a direct HTTP request to that authority. With a regular HTTPPasswordMgr, the server must know/challenge the configured realm. This does not describe normal proxy authentication: in a genuine proxy request req.host is the proxy authority.
Details
In Lib/urllib/request.py, ProxyBasicAuthHandler.http_error_407() passes req.host to http_error_auth_reqed(). For a direct HTTP request, that is the origin authority, without http://. The scheme-aware matching added for CVE-2026-15806 only rejects mismatches when both URIs have a scheme; None continues to match any scheme. The resulting credentials are placed in Proxy-Authorization and sent on the retry. The header is visible to the direct HTTP origin.
The relevant code is present in CPython 3.12.15 and remained present on the 3.12 branch checked at commit 58ed60b7415e218ce3d608302e39b5e55bfb0e88 (2026-10-01). The Lib/urllib/request.py blob in the 3.12.15 snapshot is 6320598c3ceda64addd0a0871b195ea6da21a7cf, matching the current 3.12 branch file. The 3.12.15 tests document scheme-less matching, but do not cover a direct-origin 407 handled by ProxyBasicAuthHandler.
This appears distinct from the original CVE-2026-15806 trigger (ordinary HTTP authentication matching an HTTPS credential): it is a residual path through the explicitly registered proxy-auth handler. The original fix's PR #155696 explicitly preserves scheme-less wildcard matching and has no regression test for a direct 407 response.
Reproducer
The following local-only server acts as a direct HTTP origin, not as a proxy. It first sends a 407 challenge, then records the retry header.
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Thread
from urllib.request import (
HTTPPasswordMgr, ProxyBasicAuthHandler, ProxyHandler, build_opener,
)
seen = []
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
seen.append(self.headers.get("Proxy-Authorization"))
if len(seen) == 1:
self.send_response(407)
self.send_header("Proxy-Authenticate", 'Basic realm="test-realm"')
self.end_headers()
else:
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
def log_message(self, *args):
pass
server = HTTPServer(("127.0.0.1", 0), Handler)
Thread(target=server.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{server.server_port}/resource"
https_scope = f"https://127.0.0.1:{server.server_port}/"
passwords = HTTPPasswordMgr()
passwords.add_password("test-realm", https_scope, "victim-user", "victim-secret")
opener = build_opener(
ProxyHandler({}), # explicitly no proxy
ProxyBasicAuthHandler(passwords),
)
with opener.open(url, timeout=3) as response:
print(response.status, response.read())
print(seen)
server.shutdown()
Observed on Python 3.12.13 with the CPython 3.12.15 snapshot's Lib (the tested urllib/request.py blob matches the current 3.12 branch):
200 b'ok'
[None, 'Basic dmljdGltLXVzZXI6dmljdGltLXNlY3JldA==']
The second value decodes to victim-user:victim-secret and was captured by the direct HTTP origin.
Impact
A peer able to answer the application's direct plaintext HTTP request to the same authority can receive credentials the caller registered specifically for HTTPS. This is a confidentiality impact. It is conditional on explicit proxy-auth handler configuration and same-authority/root-scope credential overlap; the default opener is not affected because it does not install ProxyBasicAuthHandler.
Suggested fix / regression test
Ensure a 407 challenge is handled as proxy authentication only when the request is actually being made through a proxy, or otherwise retain enough transport/credential-scope information to prevent a direct HTTP origin from matching HTTPS-scoped credentials. Add a regression test where a direct HTTP origin returns 407 while the password manager contains HTTPS-scoped credentials for that authority; assert that no Proxy-Authorization header is sent to the origin. Keep tests for genuine proxy authentication working.
Linked PRs
ProxyBasicAuthHandler may send HTTPS-scoped credentials to a direct HTTP origin after a 407 response
Summary
When an application explicitly installs
ProxyBasicAuthHandlerwith anHTTPPasswordMgrcontaining credentials scoped tohttps://HOST/, a direct (non-proxied) HTTP server at the same authority can return407 Proxy Authentication Requiredwith the matching realm.ProxyBasicAuthHandler.http_error_407()looks up credentials usingreq.host, which is scheme-less.HTTPPasswordMgrintentionally allows a scheme-less lookup to match credentials stored for any scheme, so the handler retries the direct HTTP request with aProxy-Authorization: Basic ...header. The direct origin receives the HTTPS-scoped password in cleartext.This requires a non-default opener configuration (the application must register
ProxyBasicAuthHandler), a credential entry for the same authority at the root path, and a direct HTTP request to that authority. With a regularHTTPPasswordMgr, the server must know/challenge the configured realm. This does not describe normal proxy authentication: in a genuine proxy requestreq.hostis the proxy authority.Details
In
Lib/urllib/request.py,ProxyBasicAuthHandler.http_error_407()passesreq.hosttohttp_error_auth_reqed(). For a direct HTTP request, that is the origin authority, withouthttp://. The scheme-aware matching added for CVE-2026-15806 only rejects mismatches when both URIs have a scheme;Nonecontinues to match any scheme. The resulting credentials are placed inProxy-Authorizationand sent on the retry. The header is visible to the direct HTTP origin.The relevant code is present in CPython 3.12.15 and remained present on the 3.12 branch checked at commit
58ed60b7415e218ce3d608302e39b5e55bfb0e88(2026-10-01). TheLib/urllib/request.pyblob in the 3.12.15 snapshot is6320598c3ceda64addd0a0871b195ea6da21a7cf, matching the current 3.12 branch file. The 3.12.15 tests document scheme-less matching, but do not cover a direct-origin 407 handled byProxyBasicAuthHandler.This appears distinct from the original CVE-2026-15806 trigger (ordinary HTTP authentication matching an HTTPS credential): it is a residual path through the explicitly registered proxy-auth handler. The original fix's PR #155696 explicitly preserves scheme-less wildcard matching and has no regression test for a direct 407 response.
Reproducer
The following local-only server acts as a direct HTTP origin, not as a proxy. It first sends a 407 challenge, then records the retry header.
Observed on Python 3.12.13 with the CPython 3.12.15 snapshot's
Lib(the testedurllib/request.pyblob matches the current 3.12 branch):The second value decodes to
victim-user:victim-secretand was captured by the direct HTTP origin.Impact
A peer able to answer the application's direct plaintext HTTP request to the same authority can receive credentials the caller registered specifically for HTTPS. This is a confidentiality impact. It is conditional on explicit proxy-auth handler configuration and same-authority/root-scope credential overlap; the default opener is not affected because it does not install
ProxyBasicAuthHandler.Suggested fix / regression test
Ensure a 407 challenge is handled as proxy authentication only when the request is actually being made through a proxy, or otherwise retain enough transport/credential-scope information to prevent a direct HTTP origin from matching HTTPS-scoped credentials. Add a regression test where a direct HTTP origin returns 407 while the password manager contains HTTPS-scoped credentials for that authority; assert that no
Proxy-Authorizationheader is sent to the origin. Keep tests for genuine proxy authentication working.Linked PRs