How a templating feature becomes remote code execution
Template engines like Jinja2, Freemarker, Twig, Velocity, and Thymeleaf exist to separate presentation from logic — a template says "insert the user's name here," and the engine fills it in safely. SSTI happens when user input is concatenated into the template string itself, rather than passed as a safely-escaped variable. At that point, the engine doesn't just display the input — it evaluates it as template syntax.
This distinction is why SSTI is scored so much higher than reflected XSS despite looking similar at first glance: XSS executes in the victim's browser with the victim's privileges. SSTI executes on your server, and most template engines expose enough of the underlying language (Python, Java, JavaScript) that "template evaluation" escalates directly to full remote code execution.
The detection probe: a math expression the engine can't resist
The standard SSTI test is deliberately simple and safe — a numeric expression that means nothing to a human but everything to a template engine. If the input is reflected as literal text, nothing happens. If it's evaluated, the math resolves:
| Engine | Syntax | Vulnerable response |
|---|---|---|
| Jinja2 / Django / Twig | {{7*7}} | 49 |
| Freemarker / Velocity / JSP EL | ${7*7} | 49 |
| Thymeleaf | [[7*7]] | 49 |
| ERB / JSP / ASP | <%= 7*7 %> | 49 |
| Spring Expression Language | #{7*7} | 49 |
A field that echoes back the literal string {{7*7}} is safe. A field that echoes back 49 is running your input as code.
HTTP proof-of-concept
1. Detection — the harmless math probe
POST /api/render-preview HTTP/1.1
Content-Type: application/json
{ "template_name": "{{7*7}}" }
HTTP/1.1 200 OK
{ "preview": "49" }
# → input evaluated as Jinja2/Twig template syntax = confirmed SSTI2. Engine fingerprinting via error disclosure
When the math probe doesn't fire cleanly, a malformed template expression often leaks the engine name in a stack trace instead:
POST /api/render-preview HTTP/1.1
{ "template_name": "{{7*'7'}}" }
HTTP/1.1 500 Internal Server Error
jinja2.exceptions.TemplateSyntaxError: unexpected char ''' at 7
# → engine confirmed as Jinja2 even though the math probe errored3. What full exploitation looks like (do not run this)
Once an engine is confirmed, published payloads escalate straight to arbitrary code execution. This is a real, documented Jinja2 sandbox-escape pattern — shown here only so you understand the actual ceiling of this vulnerability, not as something to run against a system you don't explicitly own and have authorization to test destructively:
{{ ''.__class__.__mro__[1].__subclasses__()[414]('cat /etc/passwd',shell=True,stdout=-1).communicate() }}
# → Jinja2's object introspection reaches Python's subprocess module,
# turning "render a template" into "run any shell command"Safe testing methodology
Detection and full exploitation are two very different risk levels. A responsible test stops at proof, not impact:
Start with the math probe, nothing else
{{7*7}} → 49 is complete, unambiguous proof that input reaches the template evaluator. No further payload is needed to confirm the vulnerability class.
Use error messages to fingerprint, don't force them
If a probe throws a 500 error, read the stack trace for the engine name rather than trying increasingly aggressive payloads to force a clean evaluation.
Never run the RCE-class payloads outside an authorized, destructive-testing engagement
Reading /etc/passwd or spawning a shell moves from 'confirmed vulnerability' to 'active exploitation' — treat that step as requiring its own explicit authorization, separate from a standard scan.
Report the confirmed engine and detection payload, not a working exploit chain
Engineering teams need to know the vulnerability exists and which template call site to fix — they don't need a copy-pasteable RCE payload sitting in a ticket.
Where to look: the highest-risk input surfaces
Email and notification template customization
HighFeatures letting users or admins customize email subject lines, notification text, or invoice templates often render that input directly through the app's template engine.
PDF and document generation
HighServer-side document generators frequently interpolate user-supplied fields (names, addresses, custom notes) into templates before rendering to PDF.
"Preview" or "live render" features
HighAny endpoint that renders user-provided markup or template snippets for a live preview — CMS page builders, report designers — is a direct SSTI candidate by design.
URL parameters reflected into rendered pages
MediumSearch terms, error pages, or 404 handlers that echo the requested value back through a template rather than a plain string.
Multi-tenant white-labeling / theming
MediumSystems that let tenant admins customize page templates are especially high-value: a template injection there can potentially affect other tenants sharing the render infrastructure.
CVSS 3.1 scoring
Confirmed SSTI with a working code-execution path, scope changed (impact extends beyond the vulnerable component to the underlying server):
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
AV:N / AC:L — Remotely exploitable, low complexity
PR:L — Low privileges (authenticated user reaching the render feature)
S:C — Scope changed: exploiting the template engine grants control beyond the component itself, over the underlying server
C:H / I:H / A:H — Full confidentiality, integrity, and availability impact once RCE is achieved
Score: 9.0 Critical
A template-engine error disclosure with no confirmed evaluation (fingerprinting only, no {{7*7}} confirmation) scores much lower — typically CVSS 5.3 Medium under A05:2021 Security Misconfiguration — since it discloses attack-surface information rather than confirming code execution.
Test your app for SSTI safely
Nautillo Pro runs scope-safe SSTI detection — math-probe evaluation and engine fingerprinting only, never RCE-class exploitation. Every confirmed finding includes the exact request, response, and CVSS score.
Related articles