Server-Side Template Injection: From {{7*7}} to Remote Code Execution
When user input is stitched into a server-side template, the template engine becomes an interpreter for the attacker. Here is how a harmless-looking arithmetic probe escalates to full server compromise, and how to design the bug out entirely.
{{7*7}}At a glance
CWE-1336- Interpreter
- Server-side template engine
- Required condition
- Attacker input becomes template source instead of a value passed to a fixed template.
- Potential impact
- Sensitive context disclosure and, in powerful engines, server-side code execution.
- Primary defense
- Keep templates developer-owned and pass untrusted content only as data.
Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.
On this page
Template engines like Jinja2, Twig, FreeMarker, Velocity and ERB exist to mix data into markup safely. Server-side template injection (SSTI) happens when an application does the opposite of what they were built for: it puts user input into the template source itself, rather than passing it in as data. The engine then evaluates that input as code. Because these engines expose object and reflection primitives, SSTI rarely stays an information leak — it is one of the most reliable paths to remote code execution on the web.
This post walks through how SSTI arises, how a single arithmetic probe confirms it, and how it escalates from a curiosity to full server takeover.
The mental model: template source is code
A template engine has two inputs with different trust levels. The template is a developer-authored program; the context contains values rendered by that program. Normal escaping protects context values when they become HTML, but it cannot help when a value is concatenated into the template program before parsing.
| Input placement | Engine behavior | Result of {{7*7}} |
|---|---|---|
Passed as the value of name |
Treat as data | Literal {{7*7}} |
| Concatenated into Jinja template source | Parse as Jinja syntax | 49 |
| Concatenated into a different engine | Parse using that engine's grammar | Engine-specific result |
That last row explains why detection starts with harmless fingerprinting. Jinja, Twig, FreeMarker, Velocity and ERB have different delimiters and expression behavior; reflected braces alone do not prove evaluation.
How it happens
The root cause is building a template string out of user input instead of feeding the input to the template as a variable. The mistake usually looks like convenient string formatting. Here is a Flask app that wants to greet the user by name:
from flask import request from jinja2 import Template name = request.args.get("name") template = Template("<h1>Hello " + name + "!</h1>") return template.render()
For ?name=Alice, the output is exactly what you expect:
<h1>Hello Alice!</h1>
The problem is that name is concatenated into the template source before the engine parses it. Anything the attacker writes becomes part of the template, and Jinja2 will happily evaluate template syntax it finds there.
A concrete attack
The first step is a fingerprinting probe — a small piece of arithmetic that no normal application would compute on your behalf:
GET /greet?name={{7*7}} HTTP/1.1
If the response reads Hello 49!, the engine evaluated 7*7. A literal echo would have returned Hello {{7*7}}!. That single difference proves the input is being executed as template code, not rendered as text:
<h1>Hello 49!</h1>
Once evaluation is confirmed, impact depends on the exact engine, version, configuration and objects exposed in the template context. Some engines permit only limited expressions. Others expose reflection, application configuration or helpers that can reach the underlying runtime. That is why an arithmetic result is sufficient proof for an authorized test: it establishes the broken boundary without reading secrets or attempting a sandbox escape.
The arithmetic probe is not the attack; it is the proof.
{{7*7}}returning49tells the attacker the page is an interpreter, and interpreters run whatever you give them.
Where SSTI appears
Greeting pages are teaching examples. Real sinks are more often user-editable email templates, notification builders, CMS themes, report layouts, PDF generation, support macros and “custom expression” features. A stored template can make the vulnerability second-order: the edit request looks harmless, and execution occurs later in a worker or administrator preview.
Do not confuse SSTI with client-side template injection or ordinary HTML injection. The decisive question is where evaluation occurs. SSTI is parsed on the server before the response is sent, so its reachable objects and impact belong to the server process.
What an attacker gains
Confirmed SSTI is close to a worst-case finding. Depending on the engine, an attacker can:
- Execute arbitrary commands on the server, leading to full host compromise.
- Read configuration and secrets by dumping context objects — in Jinja2,
{{ config }}often spills database credentials and API keys. - Reach the underlying runtime through reflection (
__class__,__mro__,getClass()), even when a sandbox is meant to be in the way. - Pivot internally — once code runs on the server, internal services, cloud metadata and other hosts are reachable.
Severity must be established from the reachable engine features, not assumed from the {{...}} appearance alone. Confirmed expression evaluation is always a security boundary failure; access to sensitive context or runtime primitives raises it toward critical server compromise.
The fix: pass data as context, never build templates from input
The durable fix is structural: the template must be a fixed, developer-authored string, and user input must arrive only as a variable passed into it. Never concatenate input into template source.
from flask import request, render_template_string name = request.args.get("name") return render_template_string("<h1>Hello {{ name }}!</h1>", name=name)
Here the template is constant. The name placeholder is bound to data at render time, so {{7*7}} arrives as the literal value of name and is auto-escaped to inert text — the engine never sees it as syntax. Reinforce this with:
- Logic-less or sandboxed engines where the use case allows, so even an evaluated expression cannot reach dangerous primitives.
- A strict separation of template logic (authored by developers) from template data (supplied by users) as an architectural rule, not a convention.
- Avoiding any feature that compiles or evaluates user-controlled strings as expressions, including "dynamic" template names derived from input.
Fixes that do not hold
- HTML-escaping before compilation: HTML escaping addresses markup output, not template grammar such as braces or expression delimiters.
- Blocking one probe: every engine has its own syntax, and alternate expression forms can reach the same parser.
- Removing obvious dangerous objects: context changes and engine upgrades can expose new paths; a denylist is not a durable sandbox boundary.
- Hiding the computed output: blind timing, errors or later side effects may still reveal evaluation.
- Trusting stored templates: storage does not make user-authored template source safe to execute later.
A developer review checklist
- Inventory APIs that compile or render template strings at runtime.
- Verify every template source is developer-owned or comes from a separately trusted authoring boundary.
- Pass request and stored user content only through context variables.
- Minimize the objects and helpers exposed to templates.
- Prefer logic-limited engines for genuinely user-authored layouts.
- Test with a benign, engine-specific arithmetic expression and stop once evaluation is proven.
References
How SelfSec proves this one
Scanner behaviour- Module
- SSTI
- How it probes
- Engine-fingerprinting expressions probe each template context before the payload is specialised to the engine that answered.
- How it confirms
- The rendered output has to differ from the baseline response; timing and out-of-band checks cover the engines that render nothing back.
Reported as CWE-1336 · ATT&CK T1190 · SARIF 2.1.0
A reflected response alone is not enough to promote a Server-Side Template Injection finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine
SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.
Do both things about Server-Side Template Injection
SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.