diff mbox series

[scarthgap,2/2] python3-urllib3: fix CVE-2025-50182

Message ID 20250704103539.265785-2-yogita.urade@windriver.com
State New
Headers show
Series [scarthgap,1/2] python3-urllib3: fix CVE-2025-50181 | expand

Commit Message

yurade July 4, 2025, 10:35 a.m. UTC
From: Yogita Urade <yogita.urade@windriver.com>

urllib3 is a user-friendly HTTP client library for Python. 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.

Reference:
https://nvd.nist.gov/vuln/detail/CVE-2025-50182

Upstream patch:
https://github.com/urllib3/urllib3/commit/7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f

Signed-off-by: Yogita Urade <yogita.urade@windriver.com>
---
 .../python3-urllib3/CVE-2025-50182.patch      | 195 ++++++++++++++++++
 .../python/python3-urllib3_2.2.2.bb           |   1 +
 2 files changed, 196 insertions(+)
 create mode 100644 meta/recipes-devtools/python/python3-urllib3/CVE-2025-50182.patch

Comments

Gyorgy Sarvari July 4, 2025, 11:01 a.m. UTC | #1
I think this patch isn't correct, and I believe there is a typo in the
CVE upstream description also.

1. Regarding the patch: The vulnerable method, send_jspi_request() does
not exist in the Scarthgap version. This patch basically introduces the
vulnerable new feature (which wasn't there initially) before fixing it.
2. Regarding the CVE: I think the first vulnerable version is actually
2.3.0, and the vulnerability was actually introduced with this[1] commit.

I am only ~80% sure about the 2nd comment, about 2.2.2 being not
vulnerable. In case this version is actually vulnerable, then I think
the fix should be somewhere in the middle of _StreamingFetcher.send()
method of the same file (which is only a guess after a bit of eyeballing).

[1]:
https://github.com/urllib3/urllib3/commit/8b2474d32b57f096a815ab52f9b54a843d68c140


On 7/4/25 12:35, Urade, Yogita via lists.openembedded.org wrote:
> From: Yogita Urade <yogita.urade@windriver.com>
>
> urllib3 is a user-friendly HTTP client library for Python. 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.
>
> Reference:
> https://nvd.nist.gov/vuln/detail/CVE-2025-50182
>
> Upstream patch:
> https://github.com/urllib3/urllib3/commit/7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f
>
> Signed-off-by: Yogita Urade <yogita.urade@windriver.com>
> ---
>  .../python3-urllib3/CVE-2025-50182.patch      | 195 ++++++++++++++++++
>  .../python/python3-urllib3_2.2.2.bb           |   1 +
>  2 files changed, 196 insertions(+)
>  create mode 100644 meta/recipes-devtools/python/python3-urllib3/CVE-2025-50182.patch
>
> diff --git a/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50182.patch b/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50182.patch
> new file mode 100644
> index 0000000000..d99e83a619
> --- /dev/null
> +++ b/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50182.patch
> @@ -0,0 +1,195 @@
> +From 7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f Mon Sep 17 00:00:00 2001
> +From: Illia Volochii <illia.volochii@gmail.com>
> +Date: Wed, 18 Jun 2025 16:30:35 +0300
> +Subject: [PATCH] Merge commit from fork
> +
> +Changes:
> +- Take only send_jspi_request API from commit
> +https://github.com/urllib3/urllib3/commit/8b2474d32b57f096a815ab52f9b54a843d68c140
> +
> +CVE: CVE-2025-50182
> +Upstream-Status: Backport [https://github.com/urllib3/urllib3/commit/7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f]
> +
> +Signed-off-by: Yogita Urade <yogita.urade@windriver.com>
> +---
> + docs/reference/contrib/emscripten.rst      |  2 +-
> + src/urllib3/contrib/emscripten/fetch.py    | 93 ++++++++++++++++++++++
> + test/contrib/emscripten/test_emscripten.py | 46 +++++++++++
> + 3 files changed, 140 insertions(+), 1 deletion(-)
> +
> +diff --git a/docs/reference/contrib/emscripten.rst b/docs/reference/contrib/emscripten.rst
> +index c88e422..3ff9cdd 100644
> +--- a/docs/reference/contrib/emscripten.rst
> ++++ b/docs/reference/contrib/emscripten.rst
> +@@ -68,7 +68,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:
> +diff --git a/src/urllib3/contrib/emscripten/fetch.py b/src/urllib3/contrib/emscripten/fetch.py
> +index 8d197ea..539df99 100644
> +--- a/src/urllib3/contrib/emscripten/fetch.py
> ++++ b/src/urllib3/contrib/emscripten/fetch.py
> +@@ -403,6 +403,99 @@ def send_request(request: EmscriptenRequest) -> EmscriptenResponse:
> +             raise _RequestError(err.message, request=request)
> +
> +
> ++def send_jspi_request(
> ++    request: EmscriptenRequest, streaming: bool
> ++) -> EmscriptenResponse:
> ++    """
> ++    Send a request using WebAssembly JavaScript Promise Integration
> ++    to wrap the asynchronous JavaScript fetch api (experimental).
> ++
> ++    :param request:
> ++        Request to send
> ++
> ++    :param streaming:
> ++        Whether to stream the response
> ++
> ++    :return: The response object
> ++    :rtype: EmscriptenResponse
> ++    """
> ++    timeout = request.timeout
> ++    js_abort_controller = js.AbortController.new()
> ++    headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE}
> ++    req_body = request.body
> ++    fetch_data = {
> ++        "headers": headers,
> ++        "body": to_js(req_body),
> ++        "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
> ++    # or time out.
> ++    response_js = _run_sync_with_timeout(
> ++        fetcher_promise_js,
> ++        timeout,
> ++        js_abort_controller,
> ++        request=request,
> ++        response=None,
> ++    )
> ++    headers = {}
> ++    header_iter = response_js.headers.entries()
> ++    while True:
> ++        iter_value_js = header_iter.next()
> ++        if getattr(iter_value_js, "done", False):
> ++            break
> ++        else:
> ++            headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1])
> ++    status_code = response_js.status
> ++    body: bytes | io.RawIOBase = b""
> ++
> ++    response = EmscriptenResponse(
> ++        status_code=status_code, headers=headers, body=b"", request=request
> ++    )
> ++    if streaming:
> ++        # get via inputstream
> ++        if response_js.body is not None:
> ++            # get a reader from the fetch response
> ++            body_stream_js = response_js.body.getReader()
> ++            body = _JSPIReadStream(
> ++                body_stream_js, timeout, request, response, js_abort_controller
> ++            )
> ++    else:
> ++        # get directly via arraybuffer
> ++        # n.b. this is another async JavaScript call.
> ++        body = _run_sync_with_timeout(
> ++            response_js.arrayBuffer(),
> ++            timeout,
> ++            js_abort_controller,
> ++            request=request,
> ++            response=response,
> ++        ).to_py()
> ++    response.body = body
> ++    return response
> ++
> ++
> ++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
> +diff --git a/test/contrib/emscripten/test_emscripten.py b/test/contrib/emscripten/test_emscripten.py
> +index 0e107fa..d94eda5 100644
> +--- a/test/contrib/emscripten/test_emscripten.py
> ++++ b/test/contrib/emscripten/test_emscripten.py
> +@@ -965,6 +965,52 @@ def test_redirects(
> +     )
> +
> +
> ++@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
> ++    )
> ++
> ++
> + @install_urllib3_wheel()
> + def test_insecure_requests_warning(
> +     selenium_coverage: typing.Any, testserver_http: PyodideServerInfo
> +--
> +2.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 bdb1c7ca8d..beb57010ac 100644
> --- a/meta/recipes-devtools/python/python3-urllib3_2.2.2.bb
> +++ b/meta/recipes-devtools/python/python3-urllib3_2.2.2.bb
> @@ -9,6 +9,7 @@ inherit pypi python_hatchling
>  
>  SRC_URI += " \
>      file://CVE-2025-50181.patch \
> +    file://CVE-2025-50182.patch \
>  "
>  
>  RDEPENDS:${PN} += "\
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#219915): https://lists.openembedded.org/g/openembedded-core/message/219915
> Mute This Topic: https://lists.openembedded.org/mt/113981495/6084445
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [skandigraun@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
diff mbox series

Patch

diff --git a/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50182.patch b/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50182.patch
new file mode 100644
index 0000000000..d99e83a619
--- /dev/null
+++ b/meta/recipes-devtools/python/python3-urllib3/CVE-2025-50182.patch
@@ -0,0 +1,195 @@ 
+From 7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f Mon Sep 17 00:00:00 2001
+From: Illia Volochii <illia.volochii@gmail.com>
+Date: Wed, 18 Jun 2025 16:30:35 +0300
+Subject: [PATCH] Merge commit from fork
+
+Changes:
+- Take only send_jspi_request API from commit
+https://github.com/urllib3/urllib3/commit/8b2474d32b57f096a815ab52f9b54a843d68c140
+
+CVE: CVE-2025-50182
+Upstream-Status: Backport [https://github.com/urllib3/urllib3/commit/7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f]
+
+Signed-off-by: Yogita Urade <yogita.urade@windriver.com>
+---
+ docs/reference/contrib/emscripten.rst      |  2 +-
+ src/urllib3/contrib/emscripten/fetch.py    | 93 ++++++++++++++++++++++
+ test/contrib/emscripten/test_emscripten.py | 46 +++++++++++
+ 3 files changed, 140 insertions(+), 1 deletion(-)
+
+diff --git a/docs/reference/contrib/emscripten.rst b/docs/reference/contrib/emscripten.rst
+index c88e422..3ff9cdd 100644
+--- a/docs/reference/contrib/emscripten.rst
++++ b/docs/reference/contrib/emscripten.rst
+@@ -68,7 +68,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:
+diff --git a/src/urllib3/contrib/emscripten/fetch.py b/src/urllib3/contrib/emscripten/fetch.py
+index 8d197ea..539df99 100644
+--- a/src/urllib3/contrib/emscripten/fetch.py
++++ b/src/urllib3/contrib/emscripten/fetch.py
+@@ -403,6 +403,99 @@ def send_request(request: EmscriptenRequest) -> EmscriptenResponse:
+             raise _RequestError(err.message, request=request)
+
+
++def send_jspi_request(
++    request: EmscriptenRequest, streaming: bool
++) -> EmscriptenResponse:
++    """
++    Send a request using WebAssembly JavaScript Promise Integration
++    to wrap the asynchronous JavaScript fetch api (experimental).
++
++    :param request:
++        Request to send
++
++    :param streaming:
++        Whether to stream the response
++
++    :return: The response object
++    :rtype: EmscriptenResponse
++    """
++    timeout = request.timeout
++    js_abort_controller = js.AbortController.new()
++    headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE}
++    req_body = request.body
++    fetch_data = {
++        "headers": headers,
++        "body": to_js(req_body),
++        "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
++    # or time out.
++    response_js = _run_sync_with_timeout(
++        fetcher_promise_js,
++        timeout,
++        js_abort_controller,
++        request=request,
++        response=None,
++    )
++    headers = {}
++    header_iter = response_js.headers.entries()
++    while True:
++        iter_value_js = header_iter.next()
++        if getattr(iter_value_js, "done", False):
++            break
++        else:
++            headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1])
++    status_code = response_js.status
++    body: bytes | io.RawIOBase = b""
++
++    response = EmscriptenResponse(
++        status_code=status_code, headers=headers, body=b"", request=request
++    )
++    if streaming:
++        # get via inputstream
++        if response_js.body is not None:
++            # get a reader from the fetch response
++            body_stream_js = response_js.body.getReader()
++            body = _JSPIReadStream(
++                body_stream_js, timeout, request, response, js_abort_controller
++            )
++    else:
++        # get directly via arraybuffer
++        # n.b. this is another async JavaScript call.
++        body = _run_sync_with_timeout(
++            response_js.arrayBuffer(),
++            timeout,
++            js_abort_controller,
++            request=request,
++            response=response,
++        ).to_py()
++    response.body = body
++    return response
++
++
++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
+diff --git a/test/contrib/emscripten/test_emscripten.py b/test/contrib/emscripten/test_emscripten.py
+index 0e107fa..d94eda5 100644
+--- a/test/contrib/emscripten/test_emscripten.py
++++ b/test/contrib/emscripten/test_emscripten.py
+@@ -965,6 +965,52 @@ def test_redirects(
+     )
+
+
++@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
++    )
++
++
+ @install_urllib3_wheel()
+ def test_insecure_requests_warning(
+     selenium_coverage: typing.Any, testserver_http: PyodideServerInfo
+--
+2.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 bdb1c7ca8d..beb57010ac 100644
--- a/meta/recipes-devtools/python/python3-urllib3_2.2.2.bb
+++ b/meta/recipes-devtools/python/python3-urllib3_2.2.2.bb
@@ -9,6 +9,7 @@  inherit pypi python_hatchling
 
 SRC_URI += " \
     file://CVE-2025-50181.patch \
+    file://CVE-2025-50182.patch \
 "
 
 RDEPENDS:${PN} += "\