Security context
Medium· 6.1GHSA-j8r2-6x86-q33q CVE-2023-32681CWE-200Published May 22, 2023

Unintended leak of Proxy-Authorization header in requests

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

2.3.0 → fixed in 2.31.0

Details

### Impact Since Requests v2.3.0, Requests has been vulnerable to potentially leaking `Proxy-Authorization` headers to destination servers, specifically during redirects to an HTTPS origin. This is a product of how `rebuild_proxies` is used to recompute and [reattach the `Proxy-Authorization` header](https://github.com/psf/requests/blob/f2629e9e3c7ce3c3c8c025bcd8db551101cbc773/requests/sessions.py#L319-L328) to requests when redirected. Note this behavior has _only_ been observed to affect proxied requests when credentials are supplied in the URL user information component (e.g. `https://username:password@proxy:8080`). **Current vulnerable behavior(s):** 1. HTTP → HTTPS: **leak** 2. HTTPS → HTTP: **no leak** 3. HTTPS → HTTPS: **leak** 4. HTTP → HTTP: **no leak** For HTTP connections sent through the proxy, the proxy will identify the header in the request itself and remove it prior to forwarding to the destination server. However when sent over HTTPS, the `Proxy-Authorization` header must be sent in the CONNECT request as the proxy has no visibility into further tunneled requests. This results in Requests forwarding the header to the destination server unintentionally, allowing a malicious actor to potentially exfiltrate those credentials. The reason this currently works for HTTPS connections in Requests is the `Proxy-Authorization` header is also handled by urllib3 with our usage of the ProxyManager in adapters.py with [`proxy_manager_for`](https://github.com/psf/requests/blob/f2629e9e3c7ce3c3c8c025bcd8db551101cbc773/requests/adapters.py#L199-L235). This will compute the required proxy headers in `proxy_headers` and pass them to the Proxy Manager, avoiding attaching them directly to the Request object. This will be our preferred option going forward for default usage. ### Patches Starting in Requests v2.31.0, Requests will no longer attach this header to redirects with an HTTPS destination. This should have no negative impacts on the default behavior of the l

The fix

Merge pull request from GHSA-j8r2-6x86-q33q

Nate Prewitt· May 22, 2023, 03:08 PM+23174ea7cf7a6
requests/sessions.py+3 1
@@ -324,7 +324,9 @@ def rebuild_proxies(self, prepared_request, proxies):
except KeyError:
username, password = None, None
- if username and password:
+ # urllib3 handles proxy authorization for us in the standard adapter.
+ # Avoid appending this to TLS tunneled requests where it may be leaked.
+ if not scheme.startswith('https') and username and password:
headers["Proxy-Authorization"] = _basic_auth_str(username, password)
return new_proxies
tests/test_requests.py+20 0
@@ -647,6 +647,26 @@ def test_proxy_authorization_preserved_on_request(self, httpbin):
assert sent_headers.get("Proxy-Authorization") == proxy_auth_value
+
+ @pytest.mark.parametrize(
+ "url,has_proxy_auth",
+ (
+ ('http://example.com', True),
+ ('https://example.com', False),
+ ),
+ )
+ def test_proxy_authorization_not_appended_to_https_request(self, url, has_proxy_auth):
+ session = requests.Session()
+ proxies = {
+ 'http': 'http://test:pass@localhost:8080',
+ 'https': 'http://test:pass@localhost:8090',
+ }
+ req = requests.Request('GET', url)
+ prep = req.prepare()
+ session.rebuild_proxies(prep, proxies)
+
+ assert ('Proxy-Authorization' in prep.headers) is has_proxy_auth
+
def test_basicauth_with_netrc(self, httpbin):
auth = ("user", "pass")
wrong_auth = ("wronguser", "wrongpass")

References