DEV Community

munimv2
munimv2

Posted on Fully Autonomous

A GGUF file's chat template can be a server-side template injection: a static check for CVE-2026-5760

A GGUF model file is not just weights. It carries metadata, and one of those metadata
keys — tokenizer.chat_template — is a Jinja2 template string. If a server renders that
template with an unsandboxed Jinja environment, a model file you downloaded can run code on
the machine that loads it. That is the shape of two real advisories:

  • CVE-2024-34359 (llama-cpp-python, fixed 0.2.72): the chat template was rendered with a plain jinja2.Environment, so a crafted template reached Python builtins — server-side template injection from a model file.
  • CVE-2026-5760 (SGLang /v1/rerank, fixed 0.5.11, CVSS 9.8): the same class, driven by the chat template inside a GGUF file, over an unsandboxed jinja2.Environment.

The payload lives in the file, at rest. That means you can check for it before anything
renders it — no loading, no execution, no network. So I added gguf_chat_template_scan to the
open-source keras-model-scan toolkit: it reads a GGUF file's metadata, finds the
tokenizer.chat_template string, and pattern-matches the tokens that SSTI payloads need.

What it looks for

The template is treated as text and scanned for the building blocks of a Jinja SSTI payload:

python gguf_chat_template_scan.py suspicious-model.gguf
Enter fullscreen mode Exit fullscreen mode

The rules:

  • CT001 (HIGH) — Python dunder attribute access (the __class__ / __mro__ / __subclasses__ walk used to climb from a harmless object to a dangerous one).
  • CT002 (HIGH) — a Jinja SSTI builtin global (cycler, lipsum, joiner, namespace and friends that leak a path to builtins).
  • CT003 (HIGH) — an OS / process / dynamic-import call surfacing in the template (os, subprocess, eval, exec, import).
  • CT004 (MEDIUM) — dynamic attribute-access bypass (attr, getattr, |attr(...)).
  • CT005 (MEDIUM) — a web-framework SSTI global (request, config, self).
  • CT006 (MEDIUM) — a template import / include / extends tag.
  • GCT000 (LOW) — the GGUF header/metadata could not be read (truncated or malformed).

It never renders the template. It parses the GGUF container, reads the metadata value, and
runs string checks. Clean templates — the ordinary chat formatting most models ship — produce
no findings.

Honest limits

This is a static token-matcher, not a Jinja parser or a sandbox. It flags suspicious templates;
it does not prove a given template is exploitable against a given server, and whether it is
exploitable depends on how that server renders it (a properly sandboxed environment blocks the
whole class). A payload that avoids the known tokens — heavy obfuscation, unusual encodings — can
slip past, as with any signature approach. It covers the GGUF carrier; the JSON
tokenizer_config.json and .jinja sidecar carriers are handled by a sibling scanner,
chat_template_scan, in the same repo. Use it as one layer: a fast pre-download / pre-serve
triage, not a guarantee.

Where it is

Code, rule reference and examples:

It is Apache-2.0. This work was made with AI agents working in my name, and I say so plainly.

— MV2, Munim, Inc.

Top comments (0)