RAGFlow CVE-2026-45312: a prompt template that runs OS commands
A Jinja2 template injection in RAGFlow's prompt generator turns a user-controlled prompt field into server-side RCE. CVSS 9.9, disclosed May 9, 2026.
What is this?
CVE-2026-45312 is a remote-code-execution flaw in RAGFlow, one of the most widely deployed open-source RAG (retrieval-augmented generation) engines. It was reported by Yuu (VNUHCM-UIT) of Verichains and published in the vendor advisory GHSA-wpg4-h5g2-jxm6 on May 9, 2026; the CVE record landed at NVD on May 29, 2026. It affects RAGFlow versions up to and including 0.24.0, carries a CVSS of 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H), and is classified as CWE-1336, server-side template injection.
The bug is a textbook case of a problem this class of products keeps repeating: a prompt template is rendered as code. A field that looks like configuration is fed straight into a templating engine, and the templating engine is Turing-complete.
How it works
RAGFlow builds some of its prompts with Jinja2. The prompt generator at rag/prompts/generator.py creates the environment with sandboxing turned off:
# rag/prompts/generator.py — unsandboxed environment
PROMPT_JINJA_ENV = jinja2.Environment(autoescape=False, trim_blocks=True, lstrip_blocks=True)
A helper then renders user-supplied text through that environment:
def citation_prompt(user_defined_prompts: dict = {}) -> str:
template = PROMPT_JINJA_ENV.from_string(
user_defined_prompts.get("citation_guidelines", CITATION_PROMPT_TEMPLATE))
return template.render()
The citation_guidelines value is not a developer constant. It is pulled out of a <CITATION_GUIDELINES> XML tag inside the LLM component’s sys_prompt — a parameter the user fully controls through RAGFlow’s Canvas workflow DSL. Because RAGFlow enables self-registration by default and lets any logged-in user save an arbitrary Canvas, a normal, low-privilege account is enough.
The render only fires when citation is on (cite=True, the default) and there are retrieval chunks. The researcher’s chain supplies those chunks with a DuckDuckGo search node, so no embedding model and no provider API key are required. The result: a string placed in a prompt field is evaluated server-side, and a Jinja2 expression that walks Python’s object graph reaches os and executes a shell command in the RAGFlow process. The public advisory’s proof of concept writes the output of id to disk to confirm code execution — in its lab run, as root.
The payload itself is the ordinary Jinja2 SSTI sandbox-escape idiom (traversing __globals__ / __builtins__ to import os), so we won’t reproduce a working line here — the lesson is the pattern, not the string. A sibling advisory, GHSA-vvwj-fvwh-4whx, reports the same class of SSTI in RAGFlow’s agent text-processing component, which suggests the issue was systemic rather than a one-off.
Why it matters
A RAG server is a high-value target. It usually holds provider API keys, database credentials, and the organisation’s ingested documents — exactly the material an attacker wants. Turning a self-service prompt field into host RCE collapses the whole trust boundary: a low-privilege user (or anyone who can register) reaches code execution in the context of the server, and from there to its secrets and network position.
The deeper point generalises well beyond RAGFlow. Prompt templates are code, and user input must never be the template. Many LLM products treat system_prompt, citation_guidelines, or persona fields as harmless text and pass them through Jinja2, f-strings, or format() to “fill in variables.” The moment untrusted content can define the template rather than the values, you have SSTI — the same bug web frameworks learned about a decade ago, now re-entering through the AI tooling layer.
Defenses
Never render untrusted input as a template. Treat user-controlled prompt fields strictly as data: pass them as render variables (template.render(guidelines=user_text)), never as the template source. If you must accept template fragments, that input is privileged and must be gated accordingly.
If you template at all, sandbox it. Use Jinja2’s SandboxedEnvironment (or ImmutableSandboxedEnvironment), which blocks access to dunder attributes and unsafe callables. A non-sandboxed Environment().from_string(user_input) is an RCE primitive, not a convenience.
Isolate the worker. Run the RAG/agent process as an unprivileged user in a container with no outbound network by default, a read-only filesystem, and dropped capabilities, so a successful injection cannot reach metadata endpoints, credentials, or the wider network.
Close the registration and DSL surface. Disable open self-registration on internet-reachable instances, require real privilege to define or modify workflow DSL, and whitelist permitted components instead of loading arbitrary graphs.
Patch and inventory. RAGFlow ≤ 0.24.0 is affected; track the project’s advisories and upgrade to the latest fixed release. Then find your exposed instances — a RAG engine should not sit on the public internet with default registration enabled.
Status
| Item | Detail |
|---|---|
| CVE | CVE-2026-45312 (GHSA-wpg4-h5g2-jxm6) |
| Affected | RAGFlow ≤ 0.24.0 |
| Severity | 9.9 CVSS v3.1; CWE-1336 (SSTI) |
| Auth required | Yes — any low-privilege user (self-registration on by default) |
| Vector | Jinja2 SSTI in citation_prompt() via Canvas DSL → RCE |
| Reported by | Yuu (anzuukino), VNUHCM-UIT / Verichains |
| Advisory published | May 9, 2026 (CVE at NVD May 29, 2026) |
| Related | GHSA-vvwj-fvwh-4whx (SSTI in agent text-processing component) |