hunting-server-side-template-injection · git:20260903.6dac719 · 2026-09-03 · sha256 49be960772a50939
hunting-server-side-template-injection git:20260903.6dac719A
Immutable. This exact content is served forever at /api/v1/blob/49be960772a50939.
--- name: hunting-server-side-template-injection description: >- 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. license: MIT --- # 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 1. **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. 2. **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. 3. **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. 4. **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. 5. **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. 6. **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](../../FINDING-SCHEMA.md) - source = the untrusted value reaching template compilation, sink = the template engine eval, evidence = an inert expression evaluating to its computed result.