urllib3 does not control redirects in browsers and Node.js
Description
urllib3 is a user-friendly HTTP client library for Python. Starting in version 2.2.0 and prior to 2.5.0, urllib3 does not control redirects in browsers and Node.js. urllib3 supports being used in a Pyodide runtime utilizing the JavaScript Fetch API or falling back on XMLHttpRequest. This means Python libraries can be used to make HTTP requests from a browser or Node.js. Additionally, urllib3 provides a mechanism to control redirects, but the retries and redirect parameters are ignored with Pyodide; the runtime itself determines redirect behavior. This issue has been patched in version 2.5.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
urllib3PyPI | >= 2.2.0, < 2.5.0 | 2.5.0 |
Affected products
1Patches
17eb4a2aafe49Merge commit from fork
4 files changed · +69 −1
CHANGES.rst+2 −0 modified@@ -4,6 +4,8 @@ - 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``. - TODO: add other entries in the release PR.
docs/reference/contrib/emscripten.rst+1 −1 modified@@ -65,7 +65,7 @@ Features which are usable with Emscripten support are: * Timeouts * Retries * Streaming (with Web Workers and Cross-Origin Isolation) -* Redirects (determined by browser/runtime, not restrictable with urllib3) +* Redirects (urllib3 controls redirects in Node.js but not in browsers where behavior is determined by runtime) * Decompressing response bodies Features which don't work with Emscripten:
src/urllib3/contrib/emscripten/fetch.py+20 −0 modified@@ -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
test/contrib/emscripten/test_emscripten.py+46 −0 modified@@ -960,6 +960,52 @@ def pyodide_test(selenium_coverage: typing.Any, host: str, port: int) -> None: ) +@pytest.mark.with_jspi +def test_disabled_redirects( + selenium_coverage: typing.Any, testserver_http: PyodideServerInfo +) -> None: + """ + Test that urllib3 can control redirects in Node.js. + """ + + @run_in_pyodide # type: ignore[misc] + def pyodide_test(selenium_coverage: typing.Any, host: str, port: int) -> None: + import pytest + + from urllib3 import PoolManager, request + from urllib3.contrib.emscripten.fetch import _is_node_js + from urllib3.exceptions import MaxRetryError + + if not _is_node_js(): + pytest.skip("urllib3 does not control redirects in browsers.") + + redirect_url = f"http://{host}:{port}/redirect" + + with PoolManager(retries=0) as http: + with pytest.raises(MaxRetryError): + http.request("GET", redirect_url) + + response = http.request("GET", redirect_url, redirect=False) + assert response.status == 303 + + with PoolManager(retries=False) as http: + response = http.request("GET", redirect_url) + assert response.status == 303 + + with pytest.raises(MaxRetryError): + request("GET", redirect_url, retries=0) + + response = request("GET", redirect_url, redirect=False) + assert response.status == 303 + + response = request("GET", redirect_url, retries=0, redirect=False) + assert response.status == 303 + + 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:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-48p4-8xcf-vxj5ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-50182ghsaADVISORY
- github.com/urllib3/urllib3/commit/7eb4a2aafe49a279c29b6d1f0ed0f42e9736194fghsax_refsource_MISCWEB
- github.com/urllib3/urllib3/releases/tag/2.5.0ghsax_refsource_MISCWEB
- github.com/urllib3/urllib3/security/advisories/GHSA-48p4-8xcf-vxj5ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.