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.