summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYogita Urade <yogita.urade@windriver.com>2025-07-04 16:05:38 +0530
committerSteve Sakoman <steve@sakoman.com>2025-07-11 08:11:53 -0700
commit23e57638efa26f6f65890cbe610bc557b6b33d7e (patch)
tree74dbdd391ec695c299d5320c16771eef0cc039da
parent55a6c02c353c943714c34e83043958dfeb0b552d (diff)
downloadpoky-23e57638efa26f6f65890cbe610bc557b6b33d7e.tar.gz
python3-urllib3: fix CVE-2025-50181
urllib3 is a user-friendly HTTP client library for Python. Prior to 2.5.0, it is possible to disable redirects for all requests by instantiating a PoolManager and specifying retries in a way that disable redirects. By default, requests and botocore users are not affected. An application attempting to mitigate SSRF or open redirect vulnerabilities by disabling redirects at the PoolManager level will remain vulnerable. This issue has been patched in version 2.5.0. Reference: https://nvd.nist.gov/vuln/detail/CVE-2025-50181 Upstream patch: https://github.com/urllib3/urllib3/commit/f05b1329126d5be6de501f9d1e3e36738bc08857 (From OE-Core rev: cf10eafb333daf8acfd3b8bfcb42c1fe6c26a8a5) Signed-off-by: Yogita Urade <yogita.urade@windriver.com> Signed-off-by: Steve Sakoman <steve@sakoman.com>
-rw-r--r--meta/recipes-devtools/python/python3-urllib3/CVE-2025-50181.patch283
-rw-r--r--meta/recipes-devtools/python/python3-urllib3_2.2.2.bb4
2 files changed, 287 insertions, 0 deletions
diff --git a/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50181.patch b/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50181.patch
new file mode 100644
index 0000000000..d4f6e98cc1
--- /dev/null
+++ b/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50181.patch
@@ -0,0 +1,283 @@
1From f05b1329126d5be6de501f9d1e3e36738bc08857 Mon Sep 17 00:00:00 2001
2From: Illia Volochii <illia.volochii@gmail.com>
3Date: Wed, 18 Jun 2025 16:25:01 +0300
4Subject: [PATCH] Merge commit from fork
5
6* Apply Quentin's suggestion
7
8Co-authored-by: Quentin Pradet <quentin.pradet@gmail.com>
9
10* Add tests for disabled redirects in the pool manager
11
12* Add a possible fix for the issue with not raised `MaxRetryError`
13
14* Make urllib3 handle redirects instead of JS when JSPI is used
15
16* Fix info in the new comment
17
18* State that redirects with XHR are not controlled by urllib3
19
20* Remove excessive params from new test requests
21
22* Add tests reaching max non-0 redirects
23
24* Test redirects with Emscripten
25
26* Fix `test_merge_pool_kwargs`
27
28* Add a changelog entry
29
30* Parametrize tests
31
32* Drop a fix for Emscripten
33
34* Apply Seth's suggestion to docs
35
36Co-authored-by: Seth Michael Larson <sethmichaellarson@gmail.com>
37
38* Use a minor release instead of the patch one
39
40---------
41
42Co-authored-by: Quentin Pradet <quentin.pradet@gmail.com>
43Co-authored-by: Seth Michael Larson <sethmichaellarson@gmail.com>
44
45CVE: CVE-2025-50181
46Upstream-Status: Backport [https://github.com/urllib3/urllib3/commit/f05b1329126d5be6de501f9d1e3e36738bc08857]
47
48Signed-off-by: Yogita Urade <yogita.urade@windriver.com>
49---
50 docs/reference/contrib/emscripten.rst | 2 +-
51 dummyserver/app.py | 1 +
52 src/urllib3/poolmanager.py | 18 +++-
53 test/contrib/emscripten/test_emscripten.py | 16 ++++
54 test/test_poolmanager.py | 5 +-
55 test/with_dummyserver/test_poolmanager.py | 101 +++++++++++++++++++++
56 6 files changed, 139 insertions(+), 4 deletions(-)
57
58diff --git a/docs/reference/contrib/emscripten.rst b/docs/reference/contrib/emscripten.rst
59index 9e85629..c88e422 100644
60--- a/docs/reference/contrib/emscripten.rst
61+++ b/docs/reference/contrib/emscripten.rst
62@@ -68,7 +68,7 @@ Features which are usable with Emscripten support are:
63 * Timeouts
64 * Retries
65 * Streaming (with Web Workers and Cross-Origin Isolation)
66-* Redirects
67+* Redirects (determined by browser/runtime, not restrictable with urllib3)
68 * Decompressing response bodies
69
70 Features which don't work with Emscripten:
71diff --git a/dummyserver/app.py b/dummyserver/app.py
72index 9fc9d1b..96e0dab 100644
73--- a/dummyserver/app.py
74+++ b/dummyserver/app.py
75@@ -228,6 +228,7 @@ async def encodingrequest() -> ResponseReturnValue:
76
77
78 @hypercorn_app.route("/redirect", methods=["GET", "POST", "PUT"])
79+@pyodide_testing_app.route("/redirect", methods=["GET", "POST", "PUT"])
80 async def redirect() -> ResponseReturnValue:
81 "Perform a redirect to ``target``"
82 values = await request.values
83diff --git a/src/urllib3/poolmanager.py b/src/urllib3/poolmanager.py
84index 085d1db..5763fea 100644
85--- a/src/urllib3/poolmanager.py
86+++ b/src/urllib3/poolmanager.py
87@@ -203,6 +203,22 @@ class PoolManager(RequestMethods):
88 **connection_pool_kw: typing.Any,
89 ) -> None:
90 super().__init__(headers)
91+ if "retries" in connection_pool_kw:
92+ retries = connection_pool_kw["retries"]
93+ if not isinstance(retries, Retry):
94+ # When Retry is initialized, raise_on_redirect is based
95+ # on a redirect boolean value.
96+ # But requests made via a pool manager always set
97+ # redirect to False, and raise_on_redirect always ends
98+ # up being False consequently.
99+ # Here we fix the issue by setting raise_on_redirect to
100+ # a value needed by the pool manager without considering
101+ # the redirect boolean.
102+ raise_on_redirect = retries is not False
103+ retries = Retry.from_int(retries, redirect=False)
104+ retries.raise_on_redirect = raise_on_redirect
105+ connection_pool_kw = connection_pool_kw.copy()
106+ connection_pool_kw["retries"] = retries
107 self.connection_pool_kw = connection_pool_kw
108
109 self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool]
110@@ -456,7 +472,7 @@ class PoolManager(RequestMethods):
111 kw["body"] = None
112 kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
113
114- retries = kw.get("retries")
115+ retries = kw.get("retries", response.retries)
116 if not isinstance(retries, Retry):
117 retries = Retry.from_int(retries, redirect=redirect)
118
119diff --git a/test/contrib/emscripten/test_emscripten.py b/test/contrib/emscripten/test_emscripten.py
120index 17264d8..0e107fa 100644
121--- a/test/contrib/emscripten/test_emscripten.py
122+++ b/test/contrib/emscripten/test_emscripten.py
123@@ -949,6 +949,22 @@ def test_retries(
124 pyodide_test(selenium_coverage, testserver_http.http_host, find_unused_port())
125
126
127+def test_redirects(
128+ selenium_coverage: typing.Any, testserver_http: PyodideServerInfo
129+) -> None:
130+ @run_in_pyodide # type: ignore[misc]
131+ def pyodide_test(selenium_coverage: typing.Any, host: str, port: int) -> None:
132+ from urllib3 import request
133+
134+ redirect_url = f"http://{host}:{port}/redirect"
135+ response = request("GET", redirect_url)
136+ assert response.status == 200
137+
138+ pyodide_test(
139+ selenium_coverage, testserver_http.http_host, testserver_http.http_port
140+ )
141+
142+
143 @install_urllib3_wheel()
144 def test_insecure_requests_warning(
145 selenium_coverage: typing.Any, testserver_http: PyodideServerInfo
146diff --git a/test/test_poolmanager.py b/test/test_poolmanager.py
147index ab5f203..b481a19 100644
148--- a/test/test_poolmanager.py
149+++ b/test/test_poolmanager.py
150@@ -379,9 +379,10 @@ class TestPoolManager:
151
152 def test_merge_pool_kwargs(self) -> None:
153 """Assert _merge_pool_kwargs works in the happy case"""
154- p = PoolManager(retries=100)
155+ retries = retry.Retry(total=100)
156+ p = PoolManager(retries=retries)
157 merged = p._merge_pool_kwargs({"new_key": "value"})
158- assert {"retries": 100, "new_key": "value"} == merged
159+ assert {"retries": retries, "new_key": "value"} == merged
160
161 def test_merge_pool_kwargs_none(self) -> None:
162 """Assert false-y values to _merge_pool_kwargs result in defaults"""
163diff --git a/test/with_dummyserver/test_poolmanager.py b/test/with_dummyserver/test_poolmanager.py
164index af77241..7f163ab 100644
165--- a/test/with_dummyserver/test_poolmanager.py
166+++ b/test/with_dummyserver/test_poolmanager.py
167@@ -84,6 +84,89 @@ class TestPoolManager(HypercornDummyServerTestCase):
168 assert r.status == 200
169 assert r.data == b"Dummy server!"
170
171+ @pytest.mark.parametrize(
172+ "retries",
173+ (0, Retry(total=0), Retry(redirect=0), Retry(total=0, redirect=0)),
174+ )
175+ def test_redirects_disabled_for_pool_manager_with_0(
176+ self, retries: typing.Literal[0] | Retry
177+ ) -> None:
178+ """
179+ Check handling redirects when retries is set to 0 on the pool
180+ manager.
181+ """
182+ with PoolManager(retries=retries) as http:
183+ with pytest.raises(MaxRetryError):
184+ http.request("GET", f"{self.base_url}/redirect")
185+
186+ # Setting redirect=True should not change the behavior.
187+ with pytest.raises(MaxRetryError):
188+ http.request("GET", f"{self.base_url}/redirect", redirect=True)
189+
190+ # Setting redirect=False should not make it follow the redirect,
191+ # but MaxRetryError should not be raised.
192+ response = http.request("GET", f"{self.base_url}/redirect", redirect=False)
193+ assert response.status == 303
194+
195+ @pytest.mark.parametrize(
196+ "retries",
197+ (
198+ False,
199+ Retry(total=False),
200+ Retry(redirect=False),
201+ Retry(total=False, redirect=False),
202+ ),
203+ )
204+ def test_redirects_disabled_for_pool_manager_with_false(
205+ self, retries: typing.Literal[False] | Retry
206+ ) -> None:
207+ """
208+ Check that setting retries set to False on the pool manager disables
209+ raising MaxRetryError and redirect=True does not change the
210+ behavior.
211+ """
212+ with PoolManager(retries=retries) as http:
213+ response = http.request("GET", f"{self.base_url}/redirect")
214+ assert response.status == 303
215+
216+ response = http.request("GET", f"{self.base_url}/redirect", redirect=True)
217+ assert response.status == 303
218+
219+ response = http.request("GET", f"{self.base_url}/redirect", redirect=False)
220+ assert response.status == 303
221+
222+ def test_redirects_disabled_for_individual_request(self) -> None:
223+ """
224+ Check handling redirects when they are meant to be disabled
225+ on the request level.
226+ """
227+ with PoolManager() as http:
228+ # Check when redirect is not passed.
229+ with pytest.raises(MaxRetryError):
230+ http.request("GET", f"{self.base_url}/redirect", retries=0)
231+ response = http.request("GET", f"{self.base_url}/redirect", retries=False)
232+ assert response.status == 303
233+
234+ # Check when redirect=True.
235+ with pytest.raises(MaxRetryError):
236+ http.request(
237+ "GET", f"{self.base_url}/redirect", retries=0, redirect=True
238+ )
239+ response = http.request(
240+ "GET", f"{self.base_url}/redirect", retries=False, redirect=True
241+ )
242+ assert response.status == 303
243+
244+ # Check when redirect=False.
245+ response = http.request(
246+ "GET", f"{self.base_url}/redirect", retries=0, redirect=False
247+ )
248+ assert response.status == 303
249+ response = http.request(
250+ "GET", f"{self.base_url}/redirect", retries=False, redirect=False
251+ )
252+ assert response.status == 303
253+
254 def test_cross_host_redirect(self) -> None:
255 with PoolManager() as http:
256 cross_host_location = f"{self.base_url_alt}/echo?a=b"
257@@ -138,6 +221,24 @@ class TestPoolManager(HypercornDummyServerTestCase):
258 pool = http.connection_from_host(self.host, self.port)
259 assert pool.num_connections == 1
260
261+ # Check when retries are configured for the pool manager.
262+ with PoolManager(retries=1) as http:
263+ with pytest.raises(MaxRetryError):
264+ http.request(
265+ "GET",
266+ f"{self.base_url}/redirect",
267+ fields={"target": f"/redirect?target={self.base_url}/"},
268+ )
269+
270+ # Here we allow more retries for the request.
271+ response = http.request(
272+ "GET",
273+ f"{self.base_url}/redirect",
274+ fields={"target": f"/redirect?target={self.base_url}/"},
275+ retries=2,
276+ )
277+ assert response.status == 200
278+
279 def test_redirect_cross_host_remove_headers(self) -> None:
280 with PoolManager() as http:
281 r = http.request(
282--
2832.40.0
diff --git a/meta/recipes-devtools/python/python3-urllib3_2.2.2.bb b/meta/recipes-devtools/python/python3-urllib3_2.2.2.bb
index 31a03a60b3..bdb1c7ca8d 100644
--- a/meta/recipes-devtools/python/python3-urllib3_2.2.2.bb
+++ b/meta/recipes-devtools/python/python3-urllib3_2.2.2.bb
@@ -7,6 +7,10 @@ SRC_URI[sha256sum] = "dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f
7 7
8inherit pypi python_hatchling 8inherit pypi python_hatchling
9 9
10SRC_URI += " \
11 file://CVE-2025-50181.patch \
12"
13
10RDEPENDS:${PN} += "\ 14RDEPENDS:${PN} += "\
11 python3-certifi \ 15 python3-certifi \
12 python3-cryptography \ 16 python3-cryptography \