CVE-2023-32681: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Requests is a HTTP library. Since Requests 2.3.0, Requests has been leaking Proxy-Authorization headers to destination servers when redirected to an HTTPS endpoint. This is a product of how we use `rebuild_proxies` to reattach the `Proxy-Authorization` header to requests. For HTTP connections sent through the tunnel, 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 the tunneled request. This results in Requests forwarding proxy credentials to the destination server unintentionally, allowing a malicious actor to potentially exfiltrate sensitive information. This issue has been patched in version 2.31.0.


CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 6.1
Published: 2023-05-26T18:15Z
Repository: https://github.com/psf/requests
Language: Python
Files changed: requests/sessions.py, tests/test_requests.py
Commit date: 2023-05-22 09:08:57 -0600
Commit message: Merge pull request from GHSA-j8r2-6x86-q33q


Diff: diff --git a/requests/sessions.py b/requests/sessions.py
index 6cb3b4da..dbcf2a7b 100644
--- a/requests/sessions.py
+++ b/requests/sessions.py
@@ -322,11 +322,13 @@ class SessionRedirectMixin:
         try:
             username, password = get_auth_from_url(new_proxies[scheme])
         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
 
     def rebuild_method(self, prepared_request, response):
diff --git a/tests/test_requests.py b/tests/test_requests.py
index b1c8dd45..b420c44d 100644
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -645,10 +645,30 @@ class TestRequests:
         resp = session.request(method="GET", url=httpbin("get"))
         sent_headers = resp.json().get("headers", {})
 
         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")
         url = httpbin("basic-auth", "user", "pass")
 

Vulnerable code:         if username and password:
Fixed code:         # 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:

    @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