diff --git a/backport-CVE-2025-50181-fix-suspend-redirect-ineffective.patch b/backport-CVE-2025-50181-fix-suspend-redirect-ineffective.patch deleted file mode 100644 index 8b08a1fe7c4f598abffb14abc2e1d999b71f6df4..0000000000000000000000000000000000000000 --- a/backport-CVE-2025-50181-fix-suspend-redirect-ineffective.patch +++ /dev/null @@ -1,278 +0,0 @@ -From f05b1329126d5be6de501f9d1e3e36738bc08857 Mon Sep 17 00:00:00 2001 -From: Illia Volochii -Date: Wed, 18 Jun 2025 16:25:01 +0300 -Subject: [PATCH] Merge commit from fork - -* Apply Quentin's suggestion - -Co-authored-by: Quentin Pradet - -* Add tests for disabled redirects in the pool manager - -* Add a possible fix for the issue with not raised `MaxRetryError` - -* Make urllib3 handle redirects instead of JS when JSPI is used - -* Fix info in the new comment - -* State that redirects with XHR are not controlled by urllib3 - -* Remove excessive params from new test requests - -* Add tests reaching max non-0 redirects - -* Test redirects with Emscripten - -* Fix `test_merge_pool_kwargs` - -* Add a changelog entry - -* Parametrize tests - -* Drop a fix for Emscripten - -* Apply Seth's suggestion to docs - -Co-authored-by: Seth Michael Larson - -* Use a minor release instead of the patch one - -Reference:https://github.com/urllib3/urllib3/commit/f05b1329126d5be6de501f9d1e3e36738bc08857 -Conflict:CHANGES.rst has not been modified because it is a low version -file ---- - docs/reference/contrib/emscripten.rst | 2 +- - dummyserver/app.py | 1 + - src/urllib3/poolmanager.py | 18 +++- - test/contrib/emscripten/test_emscripten.py | 16 ++++ - test/test_poolmanager.py | 5 +- - test/with_dummyserver/test_poolmanager.py | 101 +++++++++++++++++++++ - 6 files changed, 139 insertions(+), 4 deletions(-) - -diff --git a/docs/reference/contrib/emscripten.rst b/docs/reference/contrib/emscripten.rst -index 99fb20f..a8f1cda 100644 ---- a/docs/reference/contrib/emscripten.rst -+++ b/docs/reference/contrib/emscripten.rst -@@ -65,7 +65,7 @@ Features which are usable with Emscripten support are: - * Timeouts - * Retries - * Streaming (with Web Workers and Cross-Origin Isolation) --* Redirects -+* Redirects (determined by browser/runtime, not restrictable with urllib3) - * Decompressing response bodies - - Features which don't work with Emscripten: -diff --git a/dummyserver/app.py b/dummyserver/app.py -index 97b1b23..0eeb93f 100644 ---- a/dummyserver/app.py -+++ b/dummyserver/app.py -@@ -227,6 +227,7 @@ async def encodingrequest() -> ResponseReturnValue: - - - @hypercorn_app.route("/redirect", methods=["GET", "POST", "PUT"]) -+@pyodide_testing_app.route("/redirect", methods=["GET", "POST", "PUT"]) - async def redirect() -> ResponseReturnValue: - "Perform a redirect to ``target``" - values = await request.values -diff --git a/src/urllib3/poolmanager.py b/src/urllib3/poolmanager.py -index 085d1db..5763fea 100644 ---- a/src/urllib3/poolmanager.py -+++ b/src/urllib3/poolmanager.py -@@ -203,6 +203,22 @@ class PoolManager(RequestMethods): - **connection_pool_kw: typing.Any, - ) -> None: - super().__init__(headers) -+ if "retries" in connection_pool_kw: -+ retries = connection_pool_kw["retries"] -+ if not isinstance(retries, Retry): -+ # When Retry is initialized, raise_on_redirect is based -+ # on a redirect boolean value. -+ # But requests made via a pool manager always set -+ # redirect to False, and raise_on_redirect always ends -+ # up being False consequently. -+ # Here we fix the issue by setting raise_on_redirect to -+ # a value needed by the pool manager without considering -+ # the redirect boolean. -+ raise_on_redirect = retries is not False -+ retries = Retry.from_int(retries, redirect=False) -+ retries.raise_on_redirect = raise_on_redirect -+ connection_pool_kw = connection_pool_kw.copy() -+ connection_pool_kw["retries"] = retries - self.connection_pool_kw = connection_pool_kw - - self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] -@@ -456,7 +472,7 @@ class PoolManager(RequestMethods): - kw["body"] = None - kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() - -- retries = kw.get("retries") -+ retries = kw.get("retries", response.retries) - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect) - -diff --git a/test/contrib/emscripten/test_emscripten.py b/test/contrib/emscripten/test_emscripten.py -index 9317a09..5eaa674 100644 ---- a/test/contrib/emscripten/test_emscripten.py -+++ b/test/contrib/emscripten/test_emscripten.py -@@ -944,6 +944,22 @@ def test_retries( - pyodide_test(selenium_coverage, testserver_http.http_host, find_unused_port()) - - -+def test_redirects( -+ selenium_coverage: typing.Any, testserver_http: PyodideServerInfo -+) -> None: -+ @run_in_pyodide # type: ignore[misc] -+ def pyodide_test(selenium_coverage: typing.Any, host: str, port: int) -> None: -+ from urllib3 import request -+ -+ redirect_url = f"http://{host}:{port}/redirect" -+ response = request("GET", redirect_url) -+ assert response.status == 200 -+ -+ pyodide_test( -+ selenium_coverage, testserver_http.http_host, testserver_http.http_port -+ ) -+ -+ - def test_insecure_requests_warning( - selenium_coverage: typing.Any, testserver_http: PyodideServerInfo - ) -> None: -diff --git a/test/test_poolmanager.py b/test/test_poolmanager.py -index ab5f203..b481a19 100644 ---- a/test/test_poolmanager.py -+++ b/test/test_poolmanager.py -@@ -379,9 +379,10 @@ class TestPoolManager: - - def test_merge_pool_kwargs(self) -> None: - """Assert _merge_pool_kwargs works in the happy case""" -- p = PoolManager(retries=100) -+ retries = retry.Retry(total=100) -+ p = PoolManager(retries=retries) - merged = p._merge_pool_kwargs({"new_key": "value"}) -- assert {"retries": 100, "new_key": "value"} == merged -+ assert {"retries": retries, "new_key": "value"} == merged - - def test_merge_pool_kwargs_none(self) -> None: - """Assert false-y values to _merge_pool_kwargs result in defaults""" -diff --git a/test/with_dummyserver/test_poolmanager.py b/test/with_dummyserver/test_poolmanager.py -index af77241..7f163ab 100644 ---- a/test/with_dummyserver/test_poolmanager.py -+++ b/test/with_dummyserver/test_poolmanager.py -@@ -84,6 +84,89 @@ class TestPoolManager(HypercornDummyServerTestCase): - assert r.status == 200 - assert r.data == b"Dummy server!" - -+ @pytest.mark.parametrize( -+ "retries", -+ (0, Retry(total=0), Retry(redirect=0), Retry(total=0, redirect=0)), -+ ) -+ def test_redirects_disabled_for_pool_manager_with_0( -+ self, retries: typing.Literal[0] | Retry -+ ) -> None: -+ """ -+ Check handling redirects when retries is set to 0 on the pool -+ manager. -+ """ -+ with PoolManager(retries=retries) as http: -+ with pytest.raises(MaxRetryError): -+ http.request("GET", f"{self.base_url}/redirect") -+ -+ # Setting redirect=True should not change the behavior. -+ with pytest.raises(MaxRetryError): -+ http.request("GET", f"{self.base_url}/redirect", redirect=True) -+ -+ # Setting redirect=False should not make it follow the redirect, -+ # but MaxRetryError should not be raised. -+ response = http.request("GET", f"{self.base_url}/redirect", redirect=False) -+ assert response.status == 303 -+ -+ @pytest.mark.parametrize( -+ "retries", -+ ( -+ False, -+ Retry(total=False), -+ Retry(redirect=False), -+ Retry(total=False, redirect=False), -+ ), -+ ) -+ def test_redirects_disabled_for_pool_manager_with_false( -+ self, retries: typing.Literal[False] | Retry -+ ) -> None: -+ """ -+ Check that setting retries set to False on the pool manager disables -+ raising MaxRetryError and redirect=True does not change the -+ behavior. -+ """ -+ with PoolManager(retries=retries) as http: -+ response = http.request("GET", f"{self.base_url}/redirect") -+ assert response.status == 303 -+ -+ response = http.request("GET", f"{self.base_url}/redirect", redirect=True) -+ assert response.status == 303 -+ -+ response = http.request("GET", f"{self.base_url}/redirect", redirect=False) -+ assert response.status == 303 -+ -+ def test_redirects_disabled_for_individual_request(self) -> None: -+ """ -+ Check handling redirects when they are meant to be disabled -+ on the request level. -+ """ -+ with PoolManager() as http: -+ # Check when redirect is not passed. -+ with pytest.raises(MaxRetryError): -+ http.request("GET", f"{self.base_url}/redirect", retries=0) -+ response = http.request("GET", f"{self.base_url}/redirect", retries=False) -+ assert response.status == 303 -+ -+ # Check when redirect=True. -+ with pytest.raises(MaxRetryError): -+ http.request( -+ "GET", f"{self.base_url}/redirect", retries=0, redirect=True -+ ) -+ response = http.request( -+ "GET", f"{self.base_url}/redirect", retries=False, redirect=True -+ ) -+ assert response.status == 303 -+ -+ # Check when redirect=False. -+ response = http.request( -+ "GET", f"{self.base_url}/redirect", retries=0, redirect=False -+ ) -+ assert response.status == 303 -+ response = http.request( -+ "GET", f"{self.base_url}/redirect", retries=False, redirect=False -+ ) -+ assert response.status == 303 -+ - def test_cross_host_redirect(self) -> None: - with PoolManager() as http: - cross_host_location = f"{self.base_url_alt}/echo?a=b" -@@ -138,6 +221,24 @@ class TestPoolManager(HypercornDummyServerTestCase): - pool = http.connection_from_host(self.host, self.port) - assert pool.num_connections == 1 - -+ # Check when retries are configured for the pool manager. -+ with PoolManager(retries=1) as http: -+ with pytest.raises(MaxRetryError): -+ http.request( -+ "GET", -+ f"{self.base_url}/redirect", -+ fields={"target": f"/redirect?target={self.base_url}/"}, -+ ) -+ -+ # Here we allow more retries for the request. -+ response = http.request( -+ "GET", -+ f"{self.base_url}/redirect", -+ fields={"target": f"/redirect?target={self.base_url}/"}, -+ retries=2, -+ ) -+ assert response.status == 200 -+ - def test_redirect_cross_host_remove_headers(self) -> None: - with PoolManager() as http: - r = http.request( --- -2.33.0 - diff --git a/backport-CVE-2025-50182-make-retries-and-redirect-affect-in-nodejs.patch b/backport-CVE-2025-50182-make-retries-and-redirect-affect-in-nodejs.patch deleted file mode 100644 index 1f669f6407b1dbf3e6e3c41cd0fe900c4d14db72..0000000000000000000000000000000000000000 --- a/backport-CVE-2025-50182-make-retries-and-redirect-affect-in-nodejs.patch +++ /dev/null @@ -1,52 +0,0 @@ -From 7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f Mon Sep 17 00:00:00 2001 -From: Illia Volochii -Date: Wed, 18 Jun 2025 16:30:35 +0300 -Subject: [PATCH] Merge commit from fork - -Reference:https://github.com/urllib3/urllib3/commit/7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f -Conflict:test_emscripten.py has not been modified because it has been deleted in spec file.CHANGES.rst and emscripten.rst has not been modified because there are low version files now. ---- - src/urllib3/contrib/emscripten/fetch.py | 20 ++++++++++++++++++++ - 1 file changed, 20 insertions(+) - -diff --git a/src/urllib3/contrib/emscripten/fetch.py b/src/urllib3/contrib/emscripten/fetch.py -index a514306..6695821 100644 ---- a/src/urllib3/contrib/emscripten/fetch.py -+++ b/src/urllib3/contrib/emscripten/fetch.py -@@ -573,6 +573,11 @@ def send_jspi_request( - "method": request.method, - "signal": js_abort_controller.signal, - } -+ # Node.js returns the whole response (unlike opaqueredirect in browsers), -+ # so urllib3 can set `redirect: manual` to control redirects itself. -+ # https://stackoverflow.com/a/78524615 -+ if _is_node_js(): -+ fetch_data["redirect"] = "manual" - # Call JavaScript fetch (async api, returns a promise) - fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) - # Now suspend WebAssembly until we resolve that promise -@@ -693,6 +698,21 @@ def has_jspi() -> bool: - return False - - -+def _is_node_js() -> bool: -+ """ -+ Check if we are in Node.js. -+ -+ :return: True if we are in Node.js. -+ :rtype: bool -+ """ -+ return ( -+ hasattr(js, "process") -+ and hasattr(js.process, "release") -+ # According to the Node.js documentation, the release name is always "node". -+ and js.process.release.name == "node" -+ ) -+ -+ - def streaming_ready() -> bool | None: - if _fetcher: - return _fetcher.streaming_ready --- -2.33.0 - diff --git a/python-urllib3.spec b/python-urllib3.spec index e1e3d2590e06989f8eb479dfd40c1c3344803023..87862b5aa81a62baa72f6bd4e39f4edeec631d04 100644 --- a/python-urllib3.spec +++ b/python-urllib3.spec @@ -2,17 +2,14 @@ %bcond_without tests Name: python-%{srcname} -Version: 2.3.0 -Release: 4 +Version: 2.5.0 +Release: 1 Summary: Sanity-friendly HTTP client for Python License: MIT URL: https://github.com/urllib3/urllib3 Source0: %{url}/archive/%{version}/%{srcname}-%{version}.tar.gz Source1: ssl_match_hostname_py3.py -Patch001: backport-CVE-2025-50182-make-retries-and-redirect-affect-in-nodejs.patch -Patch002: backport-CVE-2025-50181-fix-suspend-redirect-ineffective.patch - BuildArch: noarch %description @@ -93,6 +90,13 @@ PYTHONPATH=%{buildroot}%{python3_sitelib}:%{python3_sitelib} %{__python3} -m pyt %{python3_sitelib}/urllib3-*.dist-info %changelog +* Wed Sep 24 2025 Yu Peng - 2.5.0-1 +- Upgrade to 2.5.0 + * Fixed a security issue where restricting the maximum number of followed redirects at the urllib3.PoolManager level via the retries parameter did not work. + * Made the Node.js runtime respect redirect parameters such as retries and redirects. + * Raised exception for HTTPResponse.shutdown on a connection already released to the pool. (#3581) + * Fixed incorrect CONNECT statement when using an IPv6 proxy with connection_from_host. Previously would not be wrapped in []. (#3615) + * Fri Aug 22 2025 tangce - 2.3.0-4 - Type:CVE - CVE:CVE-2025-50181 diff --git a/urllib3-2.3.0.tar.gz b/urllib3-2.3.0.tar.gz deleted file mode 100644 index 61650ab6e93dec08fc9a87aa50ba7f051214c4ec..0000000000000000000000000000000000000000 Binary files a/urllib3-2.3.0.tar.gz and /dev/null differ diff --git a/urllib3-2.5.0.tar.gz b/urllib3-2.5.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..39085674032a873727c3502510a138a88daf20c3 Binary files /dev/null and b/urllib3-2.5.0.tar.gz differ