Template injection (SSTI) from untrusted data
ID |
template-injection |
Severity |
critical |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
OWASP LLM |
LLM05:2025 — Improper Output Handling |
OWASP ASI |
ASI05:2026 (secondary) |
Family |
LLM05 — Improper Output Handling |
Red-team vectors |
Sandbox Escape / RCE |
Tags |
ai_security |
Description
LLM output or user-controlled data is used as the template string of a server-side template
engine: Jinja2 (Template(…), env.from_string(…), Flask render_template_string(…)), Mako,
Django templates, Handlebars (Handlebars.compile(…)) or EJS (ejs.render(…)). Template syntax
inside the data executes at render time — {{ 7*7 }} probes and
{{ import('os').system('id') }} payloads make SSTI an RCE vector equivalent in severity to
shell / eval injection (which the sibling llm-output-to-shell-or-eval
detector covers).
The sink is read off the AST call signature; the argument is checked against the bounded single-file
taint heuristic (LLM-output-looking values, user-controlled input shapes/names, and variables
assigned from either — f-string interpolations included). Data passed through the render context
of a static template is the safe pattern and produces no finding. An explicit autoescape=False
in the file is reported as an aggravating property.
Examples
search_results = agent.search(user_query) # untrusted (user-controlled)
template = Template(f"Search results: {search_results}")
output = template.render() (1)
page = env.from_string(llm_output).render() (1)
| 1 | The template string is built from untrusted data — flagged critical. |
Template("static {{ name }}").render(name=user_input) (data through the render context) produces
no finding.
const tpl = Handlebars.compile(userTemplate); // flagged
const out = ejs.render(userTemplate, data); // flagged (template arg; the data arg is safe)
Mitigation / Fix
-
Never build the template string from LLM output or user-controlled data — render a static template and pass the data through its context variables.
-
Keep autoescape enabled (
autoescape=True/select_autoescape). -
Sandbox the environment (
jinja2.sandbox.SandboxedEnvironment) for any template that must be dynamic.