SSTI: When AI Template Code Becomes Remote Code Execution
Server-Side Template Injection leads to full server compromise
Server-Side Template Injection lets attackers execute code by injecting template expressions into your app. AI tools generate vulnerable code that concatenates user input into templates. The fix: always pass user data as template variables, never embed it in the template string itself. Classified as CWE-1336.
What is Server-Side Template Injection?
Server-Side Template Injection (SSTI) is a vulnerability where user input is embedded directly into server-side templates, allowing attackers to inject malicious expressions that execute on your server. Template engines like Jinja2, EJS, and Pug process special syntax - and if attackers can inject that syntax, they control what runs on your server.
Think of templates like fill-in-the-blank documents. Normally, you fill in the blanks with data. SSTI is like letting someone rewrite the document itself. Instead of filling in "name: John", they inject code that reads your database or executes system commands.
SSTI is part of the OWASP A03:2021 Injection category. Unlike XSS which runs in browsers, SSTI runs on your server - making it typically more severe. A successful SSTI attack usually leads to Remote Code Execution (RCE), giving attackers complete control over your server.
How SSTI Attacks Work
SSTI attacks follow a predictable pattern: detection, engine identification, and exploitation. Attackers test whether template expressions are evaluated, determine which engine is running, then craft payloads specific to that engine.
Step 1: Detection
Attackers inject simple mathematical expressions to test if templates are processed:
# Test payloads
Input: {{7*7}}
Output: 49 ← SSTI confirmed!
Input: $${7*7}
Output: 49 ← SSTI confirmed!
Input: <%=7*7%>
Output: 49 ← SSTI confirmed!If the output shows "49" instead of the literal text, the template engine evaluated the expression - confirming SSTI vulnerability exists.
Step 2: Engine Identification
Different engines use different syntax. Attackers identify the engine to craft appropriate exploits:
| Syntax | Template Engines | Language |
|---|---|---|
{{...}} | Jinja2, Twig, Django | Python, PHP |
${...} | FreeMarker, Velocity | Java |
<%=...%> | EJS, ERB | Node.js, Ruby |
#{}... | Pug (Jade) | Node.js |
Step 3: Exploitation
Once the engine is identified, attackers use engine-specific payloads to achieve RCE. Here's a Jinja2 example that reads system files:
# Jinja2 payload to read /etc/passwd
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}
# Execute system commands
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}The PortSwigger SSTI guide documents exploitation techniques for various engines in detail.
Why AI Tools Generate Vulnerable Code
AI coding tools like Cursor, Claude Code, and Bolt frequently generate SSTI-vulnerable code because string concatenation feels "natural" for dynamic content. When vibe coders ask for personalized templates, AI reaches for f-strings and template literals.
# Flask/Jinja2 - AI's typical output
@app.route('/greet')
def greet():
name = request.args.get('name')
# Direct concatenation into template
template = f"<h1>Welcome, {name}!</h1>"
return render_template_string(template)
# Attack: ?name={{config.items()}}# Flask/Jinja2 - Correct approach
@app.route('/greet')
def greet():
name = request.args.get('name')
# Pass data as variable, not in template string
return render_template_string(
"<h1>Welcome, {{name}}!</h1>",
name=name
)Common AI Patterns That Create SSTI
# Pattern 1: f-string templates (Python)
template = f"Hello {user_input}"
render_template_string(template)
# Pattern 2: Template literal concatenation (Node.js)
const template = `<div>${userInput}</div>`
ejs.render(template)
# Pattern 3: String addition
template = "Hello " + user_input + "!"
render_template_string(template)Secure Patterns by Template Engine
The fix is consistent across all engines: never concatenate user input into template strings. Always pass data as separate variables.
Jinja2 (Python/Flask)
# NEVER do this
return render_template_string(
f"Hello {name}"
)# Always pass data as variables
return render_template_string(
"Hello {{name}}",
name=name
)
# Or use file-based templates (preferred)
return render_template(
'greeting.html',
name=name
)EJS (Node.js/Express)
// NEVER build template strings
const html = ejs.render(
`<p>${userInput}</p>`
)// Pass data object to template
res.render('page', {
name: userInput
})
// In template file:
// <p><%=name%></p> (escaped)How to Detect SSTI
Detection involves testing for template expression evaluation and identifying which engine is running. The OWASP SSTI Testing Guide provides comprehensive methodology.
Manual Testing
Inject these payloads into any user input field:
# Universal detection payloads
{{7*7}} → 49 = Jinja2/Twig/Django
$${7*7} → 49 = FreeMarker/Velocity
<%=7*7%> → 49 = EJS/ERB
#{}7*7 → 49 = Pug
${{7*7}} → 49 = Smarty
# Error-based detection
${{ → Error message reveals engine
$${ → Error message reveals engineAutomated Tools
- Tplmap - Automated SSTI detection and exploitation
- PayloadsAllTheThings - Payload collection for various engines
- SAST tools - Opengrep, Trivy can detect vulnerable patterns in code
SSTI vs XSS: Understanding the Difference
Both are injection vulnerabilities, but they execute in different places with vastly different impacts.
| Aspect | SSTI | XSS |
|---|---|---|
| Execution Location | Server | Browser |
| Typical Impact | Remote Code Execution | Session hijacking, defacement |
| What Attacker Accesses | Server files, databases, system commands | User's browser session, cookies |
| Severity | Critical (server compromise) | High (user account compromise) |
Important: Jinja2's autoescape prevents XSS but NOT SSTI. Learn more about XSS prevention.
AI Fix Prompt for SSTI
Copy this prompt to your AI coding tool to audit your codebase for Server-Side Template Injection vulnerabilities:
This prompt guides Cursor, Claude Code, or other AI tools through a systematic review of template usage patterns.
Notable SSTI Vulnerabilities
- CVE-2024-34359 - llama_cpp_python: Jinja2 SSTI in Python LLM bindings, allowing RCE
- CVE-2020-12790 - Twig: Template injection leading to code execution in Symfony
- CVE-2019-19999 - FreeMarker: Unsafe default configuration allowing SSTI in Java apps
Frequently Asked Questions
What is server-side template injection?
Server-side template injection (SSTI) is a vulnerability where attackers inject malicious expressions into template engines that execute on your server. Unlike XSS which runs in browsers, SSTI runs on your server - meaning attackers can read files, access databases, and execute system commands. It happens when user input is concatenated into template strings instead of passed as data.
How do I detect SSTI vulnerabilities?
Test input fields with template expressions: {{7*7}}, ${7*7}, or <%= 7*7 %>. If the output shows "49" instead of the literal text, you likely have SSTI. Different template engines use different syntax - Jinja2/Twig use {{, FreeMarker uses ${, and EJS/ERB use <%=. Error messages can also reveal which engine is running.
Is Jinja2 vulnerable to SSTI?
Jinja2 itself is not inherently vulnerable - the vulnerability comes from how developers use it. When you concatenate user input into template strings (like f"Hello {name}" with render_template_string()), you create SSTI. The secure pattern is passing data as variables: render_template_string("Hello {{ name }}", name=user_input). Jinja2's autoescape prevents XSS but does not prevent SSTI.
What's the difference between SSTI and XSS?
Location of execution is the key difference. XSS (Cross-Site Scripting) executes malicious code in the victim's browser - attackers can steal cookies or hijack sessions. SSTI (Server-Side Template Injection) executes on your server - attackers can read files, access databases, and run system commands. SSTI is typically more severe because it leads to Remote Code Execution (RCE), while XSS is limited to browser context.
How do I prevent template injection in Node.js?
Never concatenate user input into template strings. With EJS, use res.render('template', { data: userInput }) instead of building template strings dynamically. With Pug, pass data objects rather than interpolating into the template source. Avoid using eval() or Function() constructors. If you must render user-controlled templates, use a sandboxed environment and strictly validate input against an allowlist.
Related Security Topics
Scan Your Code for SSTI
vibeship scanner automatically detects template injection vulnerabilities in your AI-generated code across Jinja2, EJS, Pug, Twig, and more.
Scan Your Repository