What template engines do
You write a template with placeholders:
The engine substitutes values. The placeholders aren't just variable interpolation — modern engines support full expression evaluation, filters, even arbitrary method calls. They're small programming languages embedded in your strings.
The bug appears when the template itself — not just the values — comes from user input. The classic shape:
The user controls the company parameter and it lands directly in the template before rendering. Looks harmless. "Welcome to Acme Corp."
Then someone sends:
The page returns: Hello Marcus! Welcome to 49. Confirmed — the engine evaluated the expression. From there, escalation to RCE is mechanical.
The famous Jinja2 RCE
Jinja2's sandbox lets you traverse Python's object graph by walking from any object up to its class, then up the type hierarchy. With the right chain, you can reach os.popen or subprocess.
The exact subclass index varies per Python build, but a payload that enumerates and finds the right one is a few lines of attacker work. The SSTI Jinja payload list on PortSwigger's labs walks through this in detail.
SandboxedEnvironment. People bypass it. The Jinja maintainers themselves recommend not using user-controlled templates — the sandbox is a hardening layer, not a security boundary.Per-engine probes
Different template engines use different delimiters and expression syntax. The first attacker step is identifying which engine.
| Engine | Used by | Probe | Successful response |
|---|---|---|---|
| Jinja2 | Flask, Ansible, Salt | {{ 7*7 }} | 49 |
| Twig | Symfony, Drupal 8+ | {{ 7*7 }} | 49 |
| Smarty | Legacy PHP CMSes | {$smarty.version} | Version string |
| Mako | Python (Pylons, Pyramid) | ${7*7} | 49 |
| Velocity | Apache, older Java apps | #set($x=7*7)$x | 49 |
| Freemarker | Spring, Atlassian | ${7*7} | 49 |
| Handlebars | JavaScript | {{7*7}} | Echo (NOT 49 — Handlebars doesn't evaluate expressions, safer by default) |
| ERB | Ruby on Rails | <%= 7*7 %> | 49 |
The progression: math probe (does it evaluate?) → identify engine via syntax differences → engine-specific payload chain to RCE.
The fix
Pattern 1: never put user input into template source
The single rule that kills the bug. Templates are author-controlled. User data goes in as values, never as the template string itself.
Now {{ 7*7 }} in the company parameter is rendered as literal text, because Jinja sees it inside a value, not the template source.
Pattern 2: if you must let users supply templates, use a logic-less engine
Engines like Mustache or Handlebars deliberately don't allow arbitrary expressions. They support {{name}} substitution but not {{name.constructor.constructor("...")}} shenanigans. Use them anywhere users need template authoring (email customization, dashboard widgets, etc.).
Pattern 3: sandbox + de-privilege
If your stack demands a richer engine and you cannot remove user-template input, run the renderer in a sandboxed process with no filesystem or network access, as an unprivileged user. Even a successful injection has nowhere to go.
Real incidents
- Uber (2016) — an SSTI in their email parameter template earned a researcher a $10,000 bounty; payload was a classic Jinja2 chain.
- Atlassian Confluence (CVE-2022-26134) — OGNL injection in URL parameters; full unauthenticated RCE. Used in widespread post-disclosure attacks.
- Apache Spark UI (CVE-2022-33891) — ACL-checking code passed user input to a shell via a templated command. Pre-auth RCE.
- Velocity in older Atlassian products — multiple CVEs over the years, several leading to RCE via template injection in admin pages.
The takeaway
SSTI is what happens when a developer treats a template engine as just a string formatter. It isn't — it's a programming language embedded in your strings, and any user input that reaches the template source becomes code.
The rule: templates are author-controlled, data is user-controlled, and the two never mix. User input is always passed as a value to a static template. If you can hold that line, the vulnerability class doesn't exist in your app.
References
Formatted in APA 7.
- Kettle, J. (2015). Server-side template injection: RCE for the modern web app. PortSwigger Research. https://portswigger.net/research/server-side-template-injection
- OWASP. (2024). Server-side template injection. https://owasp.org/www-project-web-security-testing-guide/...Testing_for_Server-side_Template_Injection
- MITRE. (2024). CWE-1336: Improper neutralization of special elements used in a template engine. Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/1336.html
- National Institute of Standards and Technology. (2022). CVE-2022-26134 detail (Atlassian Confluence). National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2022-26134