← Back to case studies
CVE-2026-5760
CVSS 9.8 Critical
Python · Jinja2 · SGLang · GGUF
SGLang GGUF SSTI — Supply-Chain RCE via Model-Supplied Jinja2 Template
SGLang's rerank endpoint renders a model-supplied tokenizer.chat_template from GGUF files with an unsandboxed jinja2.Environment() — any attacker who distributes a malicious GGUF model achieves RCE on any server that loads and reranks against it.
TL;DR
// what happened
- Affected: SGLang — lmsys/sglang — a widely used open-source LLM serving framework and API server used by thousands of companies for running quantized GGUF models (Mistral, Llama, Qwen, etc.) at production scale. The reranking feature accepts user queries and scores documents using a loaded model.
- Attack vector: SGLang's rerank endpoint calls
_get_jinja_env() which returns a plain jinja2.Environment() instead of ImmutableSandboxedEnvironment. The model's tokenizer.chat_template (stored in the GGUF file's metadata) is then rendered through this unsandboxed environment. An attacker who distributes a GGUF with a malicious chat_template triggers RCE on any server that loads the model.
- Requires: The attacker only needs to publish or share a malicious GGUF file — no network access or authentication needed. The victim loads the model and makes a rerank request. This is a supply-chain attack: the vulnerability travels with the model file itself.
- Impact: Full server takeover. The
jinja2.Environment has access to all Python builtins including os, subprocess, and import. An attacker can execute arbitrary shell commands, exfiltrate data, pivot to adjacent services, or backdoor the host. The server process typically has access to model weights, secrets, and local network services.
- Fix: Replace
jinja2.Environment() with jinja2.sandbox.ImmutableSandboxedEnvironment in _get_jinja_env(). Fix PR: sgl-project/sglang#23660.
CVSS v3.1 Vector
Attack Vector (AV)
Network — no local access needed; victim loads model from anywhere
Attack Complexity (AC)
Low — a single malicious GGUF file achieves RCE; no authentication or privileged access
Privileges Required (PR)
None — attacker only needs to publish or share a GGUF file; no victim credentials needed
User Interaction (UI)
None — the rerank endpoint is a simple POST; no user action beyond loading the model
Scope (S)
Changed — RCE escapes the LLM serving context into the host process
Confidentiality (C)
High — full server access: model weights, environment variables, local network, credentials
Integrity (I)
High — modify server state, plant backdoors, corrupt model serving behavior
Availability (A)
High — RCE = full system takeover, denial of service, supply-chain poisoning
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H → 9.8
The Bug — Vulnerable Code
python/sglang/srt/entrypoints/openai/serving_rerank.py — _get_jinja_env()
Pre-fix — returns unsandboxed jinja2.Environment()
def _get_jinja_env():
try:
import jinja2
from jinja2.sandbox import ImmutableSandboxedEnvironment
except ModuleNotFoundError as e:
raise ValueError(
"Rendering Qwen3 reranker prompts requires `jinja2`. "
"Please install it in your runtime environment (e.g., `pip install jinja2`)."
) from e
# VULNERABLE: Returns a plain jinja2.Environment() instead of
# ImmutableSandboxedEnvironment. The plain Environment allows access
# to Python builtins like __import__, os, subprocess, and eval().
# When the model's tokenizer.chat_template (from a GGUF file) is rendered
# through this environment, any Jinja2 SSTI payload in the template executes
# as arbitrary Python code.
return jinja2.Environment(
loader=jinja2.BaseLoader(),
autoescape=False,
undefined=jinja2.Undefined,
# NOTE: ImmutableSandboxedEnvironment was imported above but NOT used.
# The comment even says "Using a sandboxed environment to stop malicious
# execution" — but the code returns the wrong class.
)
Exploit Chain
From GGUF model file to full server compromise
1
Attacker crafts a GGUF model file with an SSTI payload embedded in the tokenizer.chat_template metadata field. The payload uses Jinja2 expression chaining to access Python builtins: {{ cyclon.__init__.__globals__['os'].popen('id').read() }} or similar. This is indistinguishable from a legitimate quantized model file — the GGUF format's tokenizer metadata is user-controlled and not validated on load.
2
Attacker distributes the malicious GGUF via HuggingFace, model-sharing forums, or direct sharing. Victims download and load the model into SGLang using sglang.launch_server() or the /v1/rerank API. SGLang reads the tokenizer.chat_template from the GGUF metadata and stores it for use in prompt rendering.
3
Victim sends a POST to /v1/rerank with a query and document list. SGLang's reranking code calls _get_jinja_env(), gets a plain jinja2.Environment(), and renders the model's chat_template with the user-provided context (query text, document text, system prompt). The SSTI payload in the template executes as Python — the cyclon chain provides access to os.popen(), subprocess, __import__, etc.
4
Attacker achieves RCE on the SGLang server. The server process typically runs with access to model weights, environment variables (API keys, credentials), and adjacent network services. Attacker can: read all served model weights, exfiltrate secrets from the process environment, pivot to internal Kubernetes pods or cloud metadata endpoints, or plant a persistent backdoor.
The Fix — After (PR #23660, commit c6b7dd9)
Fix PR sgl-project/sglang#23660 — Use ImmutableSandboxedEnvironment
// The fix is a one-line swap: ImmutableSandboxedEnvironment instead of
// jinja2.Environment. ImmutableSandboxedEnvironment blocks access to:
// - Python builtins (os, subprocess, __import__, eval, exec, open, etc.)
// - Unsafe attribute access (__class__, __init__, __globals__, __subclasses__)
// - Module-level variables
// This prevents the SSTI exploit chain from reaching the host system.
def _get_jinja_env():
try:
import jinja2
from jinja2.sandbox import ImmutableSandboxedEnvironment
except ModuleNotFoundError as e:
raise ValueError(
"Rendering Qwen3 reranker prompts requires `jinja2`. "
"Please install it in your runtime environment (e.g., `pip install jinja2`)."
) from e
- return jinja2.Environment(
+ return ImmutableSandboxedEnvironment(
loader=jinja2.BaseLoader(),
autoescape=False,
undefined=jinja2.Undefined,
# Using a sandboxed environment to stop malicious execution during model loading.
)
Why this works: ImmutableSandboxedEnvironment restricts which attributes can be accessed in template expressions. Specifically, it blocks access to __init__, __globals__, __class__, and other dunder attributes that SSTI exploits use to break out of the template context. A payload like {{ cyclon.__init__.__globals__['os'].popen('id').read() }} now raises a SecurityError instead of executing. The environment is also immutable — templates cannot modify the underlying Python objects even if they somehow bypass the attribute restrictions.
// root cause
SGLang's rerank endpoint imports ImmutableSandboxedEnvironment from jinja2.sandbox — the comment even says "Using a sandboxed environment to stop malicious execution" — but then returns a plain jinja2.Environment() instead. This is likely a copy-paste or refactoring error: the correct class was imported but the wrong one was returned.
The plain jinja2.Environment exposes all Python builtins and allows arbitrary attribute access in template expressions. When the model's tokenizer.chat_template (which lives in the GGUF file's metadata and is user-controlled by the model author) is rendered through this environment, any Jinja2 expression in the template executes as Python. The result is a supply-chain RCE: anyone who distributes a malicious GGUF file can execute code on every server that loads it.
The root cause is a single-line error that went undetected because the GGUF model supply chain is not subject to the same security review as code. Traditional SAST tools catch SSTI in application code, but not in model metadata that flows into a template render call from a quantized model file.
// what you can do
- Upgrade SGLang → version containing PR #23660 fix (commit
c6b7dd9) — this fixes all affected versions up to 0.5.11
- Audit Jinja2 environments → search your codebase for
jinja2.Environment() calls that should use ImmutableSandboxedEnvironment instead
- Validate GGUF model sources → only load GGUF files from trusted model publishers; don't pull models from unverified sources
- Isolate LLM serving → run SGLang in a sandboxed container with minimal privileges and no access to secrets or adjacent services
- Install PullLight → catches SSTI and template injection in Python code at the PR level, including in framework wiring like this
FAQ
What's novel about this attack compared to other SSTI vulnerabilities?
Most SSTI vulnerabilities come from user input reaching a template render call in the application code. Here, the payload lives in the model file itself — the GGUF metadata field tokenizer.chat_template. This makes it a supply-chain attack: the vulnerability travels with the model. Any server that loads a malicious GGUF is compromised, regardless of whether the server's own code is secure. This is distinct from the ERPNext SSTI (CVE-2025-66434) where the attacker injects directly into a data field; here the attacker distributes the model and waits for victims to load it.
Can I trust models from HuggingFace?
Trusted publishers (Meta, Mistral AI, Qwen team, etc.) are unlikely to publish malicious models, but a compromised account or a lookalike model could distribute a weaponized GGUF. The vulnerability is in SGLang's code, not in HuggingFace, but the attack is only possible because the code processes model metadata without sandboxing. Until SGLang is patched, verify the integrity of any third-party GGUF before loading it into production.
How does this compare to the ERPNext SSTI (CVE-2025-66434)?
Both are SSTI leading to RCE. ERPNext is a direct injection: an attacker with write access to an ERP data field plants a Jinja2 payload. SGLang is a supply-chain attack: the attacker publishes a GGUF file and the RCE triggers when anyone loads it. Both stem from the same root cause — jinja2.Environment() used where ImmutableSandboxedEnvironment is needed — but the delivery mechanism is different.
Does the fix break legitimate chat templates?
ImmutableSandboxedEnvironment only blocks access to unsafe Python internals. Legitimate Jinja2 expressions like {{ messages }}, {{ system_prompt }}, or {{ query }} work normally. The only templates that break are those that intentionally access os, __import__, or other dangerous internals — which is exactly what an SSTI exploit needs. If a legitimate template uses such patterns, it should be reviewed.
Timeline
April 2026
SGLang versions up to 0.5.11 confirmed vulnerable. Rerank endpoint uses jinja2.Environment() instead of ImmutableSandboxedEnvironment in serving_rerank.py.
April 28 2026
Fix PR sgl-project/sglang#23660 merges, replacing jinja2.Environment() with ImmutableSandboxedEnvironment in _get_jinja_env(). Fix commit: c6b7dd9.
April 2026
CVE-2026-5760 assigned. Public PoC repository Stuub/SGLang-0.5.9-RCE published demonstrating the supply-chain GGUF attack.
June 2026
PullLight documents CVE-2026-5760 as case study — demonstrating supply-chain SSTI detection in Python model-serving framework code.
Don't let SSTI slip into your Python code — or your models
Fix & Resources
Fix commit: c6b7dd9 (first fix: a26af7b)
_get_jinja_env()returns a plainjinja2.Environment()instead ofImmutableSandboxedEnvironment. The importedImmutableSandboxedEnvironmentis not used. The model'stokenizer.chat_template(from GGUF metadata) is then rendered through this unsandboxed environment viajinja_env.compile_expression(...).render()or equivalent. A malicious GGUF with{{ cyclon.__init__.__globals__['os'].popen('id').read() }}or similar expression chains in thechat_templateachieves pre-auth RCE on any server that loads the model and calls/v1/rerank.This is a supply-chain attack: the vulnerability travels with the GGUF model file. A model author doesn't need to be an attacker to introduce this — an accidentally crafted template with Jinja2 expressions from a finetuning run would trigger the same path.
CVSS 9.8: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — no authentication needed; attacker only distributes a GGUF file.
jinja2.Environment()withImmutableSandboxedEnvironmentin_get_jinja_env(). Alternatively, strip{{,{%, and{#patterns from thechat_templatefield before rendering, or disable template rendering entirely for untrusted model metadata. Fix PR:sgl-project/sglang#23660.