Skip to content

Commit 8c1fac0

Browse files
[Medium] Patch python-urllib3 for CVE-2025-50181 (microsoft#14115)
1 parent c3ac246 commit 8c1fac0

File tree

2 files changed

+208
-1
lines changed

2 files changed

+208
-1
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
From 61549f2dba1beba27073f21b67c971a27e5c4708 Mon Sep 17 00:00:00 2001
2+
From: dj_palli <[email protected]>
3+
Date: Thu, 26 Jun 2025 20:53:40 +0000
4+
Subject: [PATCH] Address CVE-2025-50181
5+
6+
Upstream Patch reference: https://github.com/urllib3/urllib3/commit/f05b1329126d5be6de501f9d1e3e36738bc08857
7+
8+
---
9+
CHANGES.rst | 2 +
10+
src/urllib3/poolmanager.py | 18 +++-
11+
test/test_poolmanager.py | 5 +-
12+
test/with_dummyserver/test_poolmanager.py | 101 ++++++++++++++++++++++
13+
4 files changed, 123 insertions(+), 3 deletions(-)
14+
15+
diff --git a/CHANGES.rst b/CHANGES.rst
16+
index c45b3d1..616b801 100644
17+
--- a/CHANGES.rst
18+
+++ b/CHANGES.rst
19+
@@ -7,6 +7,8 @@ Changes
20+
- Added the ``Proxy-Authorization`` header to the list of headers to strip from requests when redirecting to a different host. As before, different headers can be set via ``Retry.remove_headers_on_redirect``.
21+
- Fixed handling of OpenSSL 3.2.0 new error message for misconfiguring an HTTP proxy as HTTPS. (`#3405 <https://github.com/urllib3/urllib3/issues/3405>`__)
22+
23+
+- Fixed a security issue where restricting the maximum number of followed redirects at the urllib3.PoolManager level via the retries parameter did not work.
24+
+- TODO: add other entries in the release PR.
25+
26+
1.26.18 (2023-10-17)
27+
--------------------
28+
diff --git a/src/urllib3/poolmanager.py b/src/urllib3/poolmanager.py
29+
index fb51bf7..a8de7c6 100644
30+
--- a/src/urllib3/poolmanager.py
31+
+++ b/src/urllib3/poolmanager.py
32+
@@ -170,6 +170,22 @@ class PoolManager(RequestMethods):
33+
34+
def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
35+
RequestMethods.__init__(self, headers)
36+
+ if "retries" in connection_pool_kw:
37+
+ retries = connection_pool_kw["retries"]
38+
+ if not isinstance(retries, Retry):
39+
+ # When Retry is initialized, raise_on_redirect is based
40+
+ # on a redirect boolean value.
41+
+ # But requests made via a pool manager always set
42+
+ # redirect to False, and raise_on_redirect always ends
43+
+ # up being False consequently.
44+
+ # Here we fix the issue by setting raise_on_redirect to
45+
+ # a value needed by the pool manager without considering
46+
+ # the redirect boolean.
47+
+ raise_on_redirect = retries is not False
48+
+ retries = Retry.from_int(retries, redirect=False)
49+
+ retries.raise_on_redirect = raise_on_redirect
50+
+ connection_pool_kw = connection_pool_kw.copy()
51+
+ connection_pool_kw["retries"] = retries
52+
self.connection_pool_kw = connection_pool_kw
53+
self.pools = RecentlyUsedContainer(num_pools)
54+
55+
@@ -389,7 +405,7 @@ class PoolManager(RequestMethods):
56+
kw["body"] = None
57+
kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
58+
59+
- retries = kw.get("retries")
60+
+ retries = kw.get("retries", response.retries)
61+
if not isinstance(retries, Retry):
62+
retries = Retry.from_int(retries, redirect=redirect)
63+
64+
diff --git a/test/test_poolmanager.py b/test/test_poolmanager.py
65+
index 61715e9..ded7b38 100644
66+
--- a/test/test_poolmanager.py
67+
+++ b/test/test_poolmanager.py
68+
@@ -322,9 +322,10 @@ class TestPoolManager(object):
69+
70+
def test_merge_pool_kwargs(self):
71+
"""Assert _merge_pool_kwargs works in the happy case"""
72+
- p = PoolManager(strict=True)
73+
+ retries = retry.Retry(total=100)
74+
+ p = PoolManager(retries=retries)
75+
merged = p._merge_pool_kwargs({"new_key": "value"})
76+
- assert {"strict": True, "new_key": "value"} == merged
77+
+ assert {"strict": retries, "new_key": "value"} == merged
78+
79+
def test_merge_pool_kwargs_none(self):
80+
"""Assert false-y values to _merge_pool_kwargs result in defaults"""
81+
diff --git a/test/with_dummyserver/test_poolmanager.py b/test/with_dummyserver/test_poolmanager.py
82+
index 02e3de5..6cb3474 100644
83+
--- a/test/with_dummyserver/test_poolmanager.py
84+
+++ b/test/with_dummyserver/test_poolmanager.py
85+
@@ -82,6 +82,89 @@ class TestPoolManager(HTTPDummyServerTestCase):
86+
assert r.status == 200
87+
assert r.data == b"Dummy server!"
88+
89+
+ @pytest.mark.parametrize(
90+
+ "retries",
91+
+ (0, Retry(total=0), Retry(redirect=0), Retry(total=0, redirect=0)),
92+
+ )
93+
+ def test_redirects_disabled_for_pool_manager_with_0(
94+
+ self, retries: typing.Literal[0] | Retry
95+
+ ) -> None:
96+
+ """
97+
+ Check handling redirects when retries is set to 0 on the pool
98+
+ manager.
99+
+ """
100+
+ with PoolManager(retries=retries) as http:
101+
+ with pytest.raises(MaxRetryError):
102+
+ http.request("GET", f"{self.base_url}/redirect")
103+
+
104+
+ # Setting redirect=True should not change the behavior.
105+
+ with pytest.raises(MaxRetryError):
106+
+ http.request("GET", f"{self.base_url}/redirect", redirect=True)
107+
+
108+
+ # Setting redirect=False should not make it follow the redirect,
109+
+ # but MaxRetryError should not be raised.
110+
+ response = http.request("GET", f"{self.base_url}/redirect", redirect=False)
111+
+ assert response.status == 303
112+
+
113+
+ @pytest.mark.parametrize(
114+
+ "retries",
115+
+ (
116+
+ False,
117+
+ Retry(total=False),
118+
+ Retry(redirect=False),
119+
+ Retry(total=False, redirect=False),
120+
+ ),
121+
+ )
122+
+ def test_redirects_disabled_for_pool_manager_with_false(
123+
+ self, retries: typing.Literal[False] | Retry
124+
+ ) -> None:
125+
+ """
126+
+ Check that setting retries set to False on the pool manager disables
127+
+ raising MaxRetryError and redirect=True does not change the
128+
+ behavior.
129+
+ """
130+
+ with PoolManager(retries=retries) as http:
131+
+ response = http.request("GET", f"{self.base_url}/redirect")
132+
+ assert response.status == 303
133+
+
134+
+ response = http.request("GET", f"{self.base_url}/redirect", redirect=True)
135+
+ assert response.status == 303
136+
+
137+
+ response = http.request("GET", f"{self.base_url}/redirect", redirect=False)
138+
+ assert response.status == 303
139+
+
140+
+ def test_redirects_disabled_for_individual_request(self) -> None:
141+
+ """
142+
+ Check handling redirects when they are meant to be disabled
143+
+ on the request level.
144+
+ """
145+
+ with PoolManager() as http:
146+
+ # Check when redirect is not passed.
147+
+ with pytest.raises(MaxRetryError):
148+
+ http.request("GET", f"{self.base_url}/redirect", retries=0)
149+
+ response = http.request("GET", f"{self.base_url}/redirect", retries=False)
150+
+ assert response.status == 303
151+
+
152+
+ # Check when redirect=True.
153+
+ with pytest.raises(MaxRetryError):
154+
+ http.request(
155+
+ "GET", f"{self.base_url}/redirect", retries=0, redirect=True
156+
+ )
157+
+ response = http.request(
158+
+ "GET", f"{self.base_url}/redirect", retries=False, redirect=True
159+
+ )
160+
+ assert response.status == 303
161+
+
162+
+ # Check when redirect=False.
163+
+ response = http.request(
164+
+ "GET", f"{self.base_url}/redirect", retries=0, redirect=False
165+
+ )
166+
+ assert response.status == 303
167+
+ response = http.request(
168+
+ "GET", f"{self.base_url}/redirect", retries=False, redirect=False
169+
+ )
170+
+ assert response.status == 303
171+
+
172+
def test_cross_host_redirect(self):
173+
with PoolManager() as http:
174+
cross_host_location = "%s/echo?a=b" % self.base_url_alt
175+
@@ -136,6 +219,24 @@ class TestPoolManager(HTTPDummyServerTestCase):
176+
pool = http.connection_from_host(self.host, self.port)
177+
assert pool.num_connections == 1
178+
179+
+ # Check when retries are configured for the pool manager.
180+
+ with PoolManager(retries=1) as http:
181+
+ with pytest.raises(MaxRetryError):
182+
+ http.request(
183+
+ "GET",
184+
+ f"{self.base_url}/redirect",
185+
+ fields={"target": f"/redirect?target={self.base_url}/"},
186+
+ )
187+
+
188+
+ # Here we allow more retries for the request.
189+
+ response = http.request(
190+
+ "GET",
191+
+ f"{self.base_url}/redirect",
192+
+ fields={"target": f"/redirect?target={self.base_url}/"},
193+
+ retries=2,
194+
+ )
195+
+ assert response.status == 200
196+
+
197+
def test_redirect_cross_host_remove_headers(self):
198+
with PoolManager() as http:
199+
r = http.request(
200+
--
201+
2.45.2
202+

SPECS/python-urllib3/python-urllib3.spec

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Summary: A powerful, sanity-friendly HTTP client for Python.
22
Name: python-urllib3
33
Version: 1.26.19
4-
Release: 1%{?dist}
4+
Release: 2%{?dist}
55
License: MIT
66
Vendor: Microsoft Corporation
77
Distribution: Mariner
@@ -25,6 +25,8 @@ BuildRequires: python3-pip
2525
%endif
2626
Requires: python3
2727

28+
Patch0: CVE-2025-50181.patch
29+
2830
%description -n python3-urllib3
2931
urllib3 is a powerful, sanity-friendly HTTP client for Python. Much of the Python ecosystem already uses urllib3 and you should too.
3032

@@ -51,6 +53,9 @@ nox --reuse-existing-virtualenvs --sessions test-%{python3_version}
5153
%{python3_sitelib}/*
5254

5355
%changelog
56+
* Thu Jun 26 2025 Durga Jagadeesh Palli <[email protected]> - 1.26.19-2
57+
- Patch CVE-2025-50181
58+
5459
* Thu Jun 20 2024 CBL-Mariner Servicing Account <[email protected]> - 1.26.19-1
5560
- Auto-upgrade to 1.26.19 - patch CVE-2024-37891
5661

0 commit comments

Comments
 (0)