Back to Technical Mode
Server-Side Template Injection (SSTI)
Templating engines (Jinja2, Handlebars, Freemarker, Twig) are designed to execute expressions embedded in {{ }} or similar delimiters. If user input is rendered AS a template (rather than passed in as a data value to a template), an attacker's input is evaluated as template code — and many engines expose enough functionality through that expression syntax to reach full code execution.
The tell-tale test is simple: submit ${7*7} or {{7*7}} — if the response contains 49 instead of the literal string, the input is being evaluated as a template, not treated as data
SSTI is easy to confuse with XSS (both often surface via a text field reflected in a page) but is far more severe — XSS runs in the victim's browser, SSTI runs on the server
The vulnerability is calling the template engine's render function on untrusted input directly, not on any specific piece of syntax — sandboxing the syntax is fragile, never rendering untrusted input as a template at all is not
Many popular template engines' expression languages are Turing-complete or expose enough of the underlying language's object model to reach arbitrary code execution from a seemingly simple expression
Want this flow explained in detail as it plays?
POST/api/generate-report
Headers
Host:app.flowground.dev
Content-Type:application/json
Body
Loading...
Quick-Insert Payloads
Defense in depth
- Never pass user input into a template engine's render/compile function as if it were template source — always pass it as a data variable to a separately-authored, trusted template
- If a feature genuinely requires user-customizable templates, use a logic-less templating language with no code-execution primitives at all (e.g. Mustache's logic-less mode) rather than a full expression-capable engine
- Run the templating step in a sandboxed environment with no access to dangerous classes/objects, as defense-in-depth even when user input is believed to be handled correctly
Vulnerable vs. Fixed
const template = `Report title: ${req.body.reportTitle}`; // user input concatenated into the template SOURCE
res.send(templateEngine.render(template)); // rendered as a template, not as data