CVE-2025-62593
Description
Ray is an AI compute engine. Prior to version 2.52.0, developers working with Ray as a development tool can be exploited via a critical RCE vulnerability exploitable via Firefox and Safari. This vulnerability is due to an insufficient guard against browser-based attacks, as the current defense uses the User-Agent header starting with the string "Mozilla" as a defense mechanism. This defense is insufficient as the fetch specification allows the User-Agent header to be modified. Combined with a DNS rebinding attack against the browser, and this vulnerability is exploitable against a developer running Ray who inadvertently visits a malicious website, or is served a malicious advertisement (malvertising). This issue has been patched in version 2.52.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
rayPyPI | < 2.52.0 | 2.52.0 |
Affected products
1- Range: ray-0.1.0, ray-0.1.1, ray-0.1.2, …
Patches
170e7c72780bdAdd denial of fetch headers (#58553)
3 files changed · +80 −2
python/ray/dashboard/http_server_head.py+4 −1 modified@@ -307,7 +307,10 @@ async def browsers_no_post_put_middleware(request, handler): if ( # A best effort test for browser traffic. All common browsers # start with Mozilla at the time of writing. - dashboard_optional_utils.is_browser_request(request) + ( + dashboard_optional_utils.is_browser_request(request) + or dashboard_optional_utils.has_sec_fetch_headers(request) + ) and request.method in [hdrs.METH_POST, hdrs.METH_PUT] ): return aiohttp.web.Response(
python/ray/dashboard/optional_utils.py+18 −0 modified@@ -138,12 +138,30 @@ def is_browser_request(req: Request) -> bool: return req.headers["User-Agent"].startswith("Mozilla") +def has_sec_fetch_headers(req: Request) -> bool: + """Checks for the existance of any of the sec-fetch-* headers""" + return any( + h in req.headers + for h in ( + "Sec-Fetch-Mode", + "Sec-Fetch-Dest", + "Sec-Fetch-Site", + "Sec-Fetch-User", + ) + ) + + def deny_browser_requests() -> Callable: """Reject any requests that appear to be made by a browser""" def decorator_factory(f: Callable) -> Callable: @functools.wraps(f) async def decorator(self, req: Request): + if has_sec_fetch_headers(req): + return Response( + text="Browser requests not allowed", + status=aiohttp.web.HTTPMethodNotAllowed.status_code, + ) if is_browser_request(req): return Response( text="Browser requests not allowed",
python/ray/dashboard/tests/test_dashboard.py+58 −1 modified@@ -419,10 +419,16 @@ def test_browser_no_post_no_put(enable_test_module, ray_start_with_dashboard): webui_url = ray_start_with_dashboard["webui_url"] webui_url = format_web_url(webui_url) + def dashboard_available(): + try: + return requests.get(webui_url).status_code == 200 + except Exception: + return False + timeout_seconds = 30 start_time = time.time() + wait_for_condition(dashboard_available) while True: - time.sleep(3) try: # Starting and getting jobs should be fine from API clients response = requests.post( @@ -458,6 +464,57 @@ def test_browser_no_post_no_put(enable_test_module, ray_start_with_dashboard): raise Exception("Timed out while testing.") +@pytest.mark.skipif( + os.environ.get("RAY_MINIMAL") == "1", + reason="This test is not supposed to work for minimal installation.", +) +def test_deny_fetch_requests(enable_test_module, ray_start_with_dashboard): + assert wait_until_server_available(ray_start_with_dashboard["webui_url"]) is True + webui_url = ray_start_with_dashboard["webui_url"] + webui_url = format_web_url(webui_url) + + def dashboard_available(): + try: + return requests.get(webui_url).status_code == 200 + except Exception: + return False + + timeout_seconds = 30 + start_time = time.time() + wait_for_condition(dashboard_available) + while True: + try: + # Starting and getting jobs should be fine from API clients + response = requests.post( + webui_url + "/api/jobs/", json={"entrypoint": "ls"} + ) + response.raise_for_status() + response = requests.get(webui_url + "/api/jobs/") + response.raise_for_status() + + # Starting job should be blocked for browsers + response = requests.post( + webui_url + "/api/jobs/", + json={"entrypoint": "ls"}, + headers={ + "User-Agent": ("Spurious User Agent"), + "Sec-Fetch-Site": ("cross-site"), + }, + ) + with pytest.raises(HTTPError): + response.raise_for_status() + + # Getting jobs should be fine for browsers + response = requests.get(webui_url + "/api/jobs/") + response.raise_for_status() + break + except (AssertionError, requests.exceptions.ConnectionError) as e: + logger.info("Retry because of %s", e) + finally: + if time.time() > start_time + timeout_seconds: + raise Exception("Timed out while testing.") + + @pytest.mark.skipif( os.environ.get("RAY_MINIMAL") == "1", reason="This test is not supposed to work for minimal installation.",
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
9- github.com/advisories/GHSA-q279-jhrf-cc6vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-62593ghsaADVISORY
- docs.ray.io/en/releases-2.51.1/ray-security/index.htmlghsaWEB
- en.wikipedia.org/wiki/MalvertisingghsaWEB
- github.com/nccgroup/singularity/pull/68ghsaWEB
- github.com/ray-project/ray/blob/e7889ae542bf0188610bc8b06d274cbf53790cbd/python/ray/dashboard/http_server_head.pyghsaWEB
- github.com/ray-project/ray/blob/f39a860436dca3ed5b9dfae84bd867ac10c84dc6/python/ray/dashboard/optional_utils.pyghsaWEB
- github.com/ray-project/ray/commit/70e7c72780bdec075dba6cad1afe0832772bfe09nvdWEB
- github.com/ray-project/ray/security/advisories/GHSA-q279-jhrf-cc6vnvdWEB
News mentions
0No linked articles in our index yet.