Hunting server-side template injection: when input becomes the template, not the data
Template engines are safe when untrusted input is passed as a bound variable into a fixed, precompiled
template: the value can only ever be data. Server-side template injection happens when input instead becomes
part of the template that the engine compiles, either concatenated into a template string, supplied as the
template name or path, or authored directly by a user in a feature that renders their markup. Now the engine
evaluates the attacker's template expressions. The floor is expression evaluation and data disclosure; the
ceiling, on many engines, is remote code execution by walking object attributes to reach a runtime, a
loader, or a process API, and by escaping whatever sandbox the engine offers. The distinction that governs
everything is whether input reaches compilation or only reaches rendering as data.
When to use
- Code builds a template by concatenating or interpolating untrusted input before handing it to the engine.
- A template name, path, or layout is selected from user input.
- A feature lets users author templates or expressions: email or report templates, themes, or rules.
Scope check
Test template injection only against applications you own or are authorized to assess, on non-production
infrastructure, because a confirmed case often reaches code execution. Prove the class with an inert
arithmetic or string expression before attempting anything further, and stay within the authorized instance.
If you can't name the authorization, stop.
The loop
Establish that input reaches compilation first. Determine whether the untrusted value is compiled as
part of a template (a dynamic template string, a user-chosen template name, or user-authored markup) or
is only bound as a variable into a fixed template. This is the false-positive killer: a value passed as
data into a precompiled template is not template injection no matter how it renders. Confirm the value
reaches the engine's compile or eval step before proceeding.
Identify the engine and its evaluation model. Name the template engine, because the syntax, the
reachable internals, and the sandbox differ sharply between them. Establish whether expressions can read
arbitrary object attributes, call methods, or reach a runtime, and whether a sandbox or a restricted mode
is configured.
Confirm evaluation with an inert probe. Establish the class with a benign expression whose evaluated
result differs from its literal text, for example an arithmetic or string-concatenation expression in the
engine's syntax. Evaluation of the expression, not reflection of the literal characters, is what proves
injection and separates it from reflected XSS.
Map the reachable capability. From a confirmed expression, determine what the engine exposes: reading
configuration and context variables, traversing object graphs to sensitive data, invoking methods, or
reaching a loader or process API. This is where the impact is decided, from disclosure to code execution;
assess it by reading what the engine and the template context make reachable, not by launching payloads.
Check the sandbox and the input path. Determine whether a configured sandbox actually blocks attribute
access and method calls or is a known-porous one, and whether the template-name path is constrained to an
allowlist of known templates rather than an attacker-chosen file. A strong sandbox plus data-only binding
plus a fixed template set is what closes the class.
Confirm and record. Confirm by evaluating an inert expression through the sink on an authorized
instance and, where the engine allows, demonstrating a bounded read of a non-sensitive context value.
Kill the lead if input is only bound as data into a fixed template, if the template name is constrained
to an allowlist, if the engine's sandbox provably blocks attribute and method access for the reachable
context, or if the value is escaped as output rather than compiled. Record the input, the compilation
sink, the engine, and the reachable capability.
Where template injection leaks
- Data binding is safe; string building is not. The same engine is secure with a bound variable and
injectable when input is concatenated into the template source.
- Template names select attacker files. A user-controlled template name or path can load an unintended
or attacker-supplied template even when the values inside are bound safely.
- The engine decides the ceiling. Some engines evaluate only simple expressions; others expose object
internals that reach a runtime, so the same primitive is disclosure on one and code execution on another.
- Sandboxes vary and leak. A restricted mode that forgets one attribute or one builtin is an escape;
treat a sandbox as a control to verify, not a guarantee.
- It hides in author features. Email, report, theme, and rule builders that accept template markup are
injection by design unless the expression surface is deliberately constrained.
Worked example (a confirm and a kill)
Confirm. A notification feature lets an admin write a message template that the server renders by
concatenating the stored string into the engine and compiling it. An inert arithmetic expression in the
engine's syntax evaluates to its computed value in the sent message, and the template context exposes
object attributes that reach a loader. Confirmed server-side template injection with a path to code
execution, critical, remediation = render user content as bound data in a fixed template, never compile
user strings, and if authoring is required use a logic-less engine or an expression allowlist with no
attribute or method access.
Kill. The message body is passed as a bound variable into a precompiled, logic-less template, the
template name is selected from a fixed server-side allowlist, and the engine runs in a restricted mode
that blocks attribute and method access. An arithmetic expression renders as literal text. Killed,
kill_reason = "input is bound as data into a fixed template, template names are allowlisted, and the
engine cannot evaluate attacker expressions; no input reaches compilation."
Rationalizations to reject
- "We escape the output, so it is safe." -> Output escaping stops XSS in the rendered result; it does
nothing when the input is compiled as template source and evaluated first.
- "Only admins can edit templates." -> An authenticated author is still an attacker for this class, and
author features reach code execution; constrain the expression surface regardless of who edits.
- "The sandbox blocks dangerous calls." -> Sandboxes for these engines are frequently escaped; verify the
exact restricted mode against the reachable context rather than trusting it.
- "It just reflects the input." -> Reflection is XSS; evaluation of an expression is template injection.
Prove which with an inert expression whose result differs from its text.
- "The template name is internal." -> If any request influences the name or path, it can select an
unintended or attacker-controlled template; pin it to an allowlist.
Executing this in practice
You need every place a template is compiled or selected, which of those take untrusted input as source
versus bound data, the engine in use, and whether a sandbox or restricted mode is configured. For each
compilation sink, decide whether input reaches it and what the engine and context make reachable. Reading the
call that builds and compiles the template shows whether input is source or data; an inert expression through
the sink shows whether the engine evaluates it.
Related
hunting-expression-language-injection - the closely related class where the evaluated language is an
expression language embedded in a framework rather than a full template engine.
hunting-reflected-and-stored-xss - the lesser sibling to rule out; reflection of literal input is XSS,
evaluation of an expression is template injection.
hunting-os-command-injection - a frequent escalation target once an engine exposes a process API through
a reachable object graph.
adjudicating-taint-paths - use it to prove the input reaches a compile or eval step and not just a bound
render.
- FINDING-SCHEMA.md - source = the untrusted value reaching template compilation,
sink = the template engine eval, evidence = an inert expression evaluating to its computed result.
1---2name: hunting-server-side-template-injection3description: Hunt server-side template injection where untrusted input becomes part of a template that the engine compiles and evaluates, rather than data passed into a fixed precompiled template. Covers input concatenated into a template string, a user-chosen template name or path, and admin or content features that render user-authored templates, across engines like Jinja, Twig, Freemarker, Velocity, ERB, and Handlebars. The impact runs from expression evaluation and data disclosure up to remote code execution when the engine exposes object internals or a weak sandbox. Use when a template string or name is built from input, or a feature lets users supply template markup. The untrusted value reaching compilation is the source, the template engine eval is the sink, and expression evaluation escalating to disclosure or code execution is the bug.4license: MIT5---67# Hunting server-side template injection: when input becomes the template, not the data89Template engines are safe when untrusted input is passed as a bound variable into a fixed, precompiled10template: the value can only ever be data. Server-side template injection happens when input instead becomes11part of the template that the engine compiles, either concatenated into a template string, supplied as the12template name or path, or authored directly by a user in a feature that renders their markup. Now the engine13evaluates the attacker's template expressions. The floor is expression evaluation and data disclosure; the14ceiling, on many engines, is remote code execution by walking object attributes to reach a runtime, a15loader, or a process API, and by escaping whatever sandbox the engine offers. The distinction that governs16everything is whether input reaches compilation or only reaches rendering as data.1718## When to use1920- Code builds a template by concatenating or interpolating untrusted input before handing it to the engine.21- A template name, path, or layout is selected from user input.22- A feature lets users author templates or expressions: email or report templates, themes, or rules.2324## Scope check2526Test template injection only against applications you own or are authorized to assess, on non-production27infrastructure, because a confirmed case often reaches code execution. Prove the class with an inert28arithmetic or string expression before attempting anything further, and stay within the authorized instance.29If you can't name the authorization, stop.3031## The loop32331. **Establish that input reaches compilation first.** Determine whether the untrusted value is compiled as34 part of a template (a dynamic template string, a user-chosen template name, or user-authored markup) or35 is only bound as a variable into a fixed template. This is the false-positive killer: a value passed as36 data into a precompiled template is not template injection no matter how it renders. Confirm the value37 reaches the engine's compile or eval step before proceeding.38392. **Identify the engine and its evaluation model.** Name the template engine, because the syntax, the40 reachable internals, and the sandbox differ sharply between them. Establish whether expressions can read41 arbitrary object attributes, call methods, or reach a runtime, and whether a sandbox or a restricted mode42 is configured.43443. **Confirm evaluation with an inert probe.** Establish the class with a benign expression whose evaluated45 result differs from its literal text, for example an arithmetic or string-concatenation expression in the46 engine's syntax. Evaluation of the expression, not reflection of the literal characters, is what proves47 injection and separates it from reflected XSS.48494. **Map the reachable capability.** From a confirmed expression, determine what the engine exposes: reading50 configuration and context variables, traversing object graphs to sensitive data, invoking methods, or51 reaching a loader or process API. This is where the impact is decided, from disclosure to code execution;52 assess it by reading what the engine and the template context make reachable, not by launching payloads.53545. **Check the sandbox and the input path.** Determine whether a configured sandbox actually blocks attribute55 access and method calls or is a known-porous one, and whether the template-name path is constrained to an56 allowlist of known templates rather than an attacker-chosen file. A strong sandbox plus data-only binding57 plus a fixed template set is what closes the class.58596. **Confirm and record.** Confirm by evaluating an inert expression through the sink on an authorized60 instance and, where the engine allows, demonstrating a bounded read of a non-sensitive context value.61 Kill the lead if input is only bound as data into a fixed template, if the template name is constrained62 to an allowlist, if the engine's sandbox provably blocks attribute and method access for the reachable63 context, or if the value is escaped as output rather than compiled. Record the input, the compilation64 sink, the engine, and the reachable capability.6566## Where template injection leaks6768- **Data binding is safe; string building is not.** The same engine is secure with a bound variable and69 injectable when input is concatenated into the template source.70- **Template names select attacker files.** A user-controlled template name or path can load an unintended71 or attacker-supplied template even when the values inside are bound safely.72- **The engine decides the ceiling.** Some engines evaluate only simple expressions; others expose object73 internals that reach a runtime, so the same primitive is disclosure on one and code execution on another.74- **Sandboxes vary and leak.** A restricted mode that forgets one attribute or one builtin is an escape;75 treat a sandbox as a control to verify, not a guarantee.76- **It hides in author features.** Email, report, theme, and rule builders that accept template markup are77 injection by design unless the expression surface is deliberately constrained.7879## Worked example (a confirm and a kill)8081> **Confirm.** A notification feature lets an admin write a message template that the server renders by82> concatenating the stored string into the engine and compiling it. An inert arithmetic expression in the83> engine's syntax evaluates to its computed value in the sent message, and the template context exposes84> object attributes that reach a loader. **Confirmed** server-side template injection with a path to code85> execution, `critical`, remediation = render user content as bound data in a fixed template, never compile86> user strings, and if authoring is required use a logic-less engine or an expression allowlist with no87> attribute or method access.88>89> **Kill.** The message body is passed as a bound variable into a precompiled, logic-less template, the90> template name is selected from a fixed server-side allowlist, and the engine runs in a restricted mode91> that blocks attribute and method access. An arithmetic expression renders as literal text. **Killed**,92> `kill_reason` = "input is bound as data into a fixed template, template names are allowlisted, and the93> engine cannot evaluate attacker expressions; no input reaches compilation."9495## Rationalizations to reject9697- *"We escape the output, so it is safe."* -> Output escaping stops XSS in the rendered result; it does98 nothing when the input is compiled as template source and evaluated first.99- *"Only admins can edit templates."* -> An authenticated author is still an attacker for this class, and100 author features reach code execution; constrain the expression surface regardless of who edits.101- *"The sandbox blocks dangerous calls."* -> Sandboxes for these engines are frequently escaped; verify the102 exact restricted mode against the reachable context rather than trusting it.103- *"It just reflects the input."* -> Reflection is XSS; evaluation of an expression is template injection.104 Prove which with an inert expression whose result differs from its text.105- *"The template name is internal."* -> If any request influences the name or path, it can select an106 unintended or attacker-controlled template; pin it to an allowlist.107108## Executing this in practice109110You need every place a template is compiled or selected, which of those take untrusted input as source111versus bound data, the engine in use, and whether a sandbox or restricted mode is configured. For each112compilation sink, decide whether input reaches it and what the engine and context make reachable. Reading the113call that builds and compiles the template shows whether input is source or data; an inert expression through114the sink shows whether the engine evaluates it.115116## Related117118- `hunting-expression-language-injection` - the closely related class where the evaluated language is an119 expression language embedded in a framework rather than a full template engine.120- `hunting-reflected-and-stored-xss` - the lesser sibling to rule out; reflection of literal input is XSS,121 evaluation of an expression is template injection.122- `hunting-os-command-injection` - a frequent escalation target once an engine exposes a process API through123 a reachable object graph.124- `adjudicating-taint-paths` - use it to prove the input reaches a compile or eval step and not just a bound125 render.126- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the untrusted value reaching template compilation,127 sink = the template engine eval, evidence = an inert expression evaluating to its computed result.