Skip to content

Direct HTTP 407 can receive HTTPS-scoped credentials #158907

Description

@bupt-Yy-young

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.11only security fixes3.12only security fixes3.13only security fixes3.14bugs and security fixes3.15pre-release feature fixes, bugs and security fixes3.16new features, bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytype-securityA security issue

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions