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.
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}} returning 49 tells 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.
Module: SSTI. How it probes: Engine-fingerprinting expressions probe each template context before the payload is specialized 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. A finding moves from Possible to Confirmed only after this check. Reported as CWE-1336 · ATT&CK T1190 · SARIF 2.1.0.
How SelfSec proves this one
PossibleConfirmed
Module
SSTI
How it probes
Engine-fingerprinting expressions probe each template context before the payload is specialized 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 asCWE-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 behavior before it reaches the report. See the detection engine
Find Server-Side Template Injection in your own app
Reading about the class is one thing. SelfSec tests for it against an application you are authorized to assess, and only calls it real when independent evidence agrees.