Critical Severity CWE-1336 OWASP A03:2021

SSTI: When AI Template Code Becomes Remote Code Execution

Server-Side Template Injection leads to full server compromise

Quick Answer

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.

RCE
Typical Impact
Critical
Severity
#3
OWASP Injection
8+
Major Engines Affected

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:

SyntaxTemplate EnginesLanguage
{{...}}Jinja2, Twig, DjangoPython, PHP
${...}FreeMarker, VelocityJava
<%=...%>EJS, ERBNode.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.

AI-Generated (Vulnerable)
# 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()}}
Secure Pattern
# 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)

Vulnerable
# NEVER do this
return render_template_string(
    f"Hello {name}"
)
Secure
# 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)

Vulnerable
// NEVER build template strings
const html = ejs.render(
    `<p>${userInput}</p>`
)
Secure
// 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 engine

Automated 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.

AspectSSTIXSS
Execution LocationServerBrowser
Typical ImpactRemote Code ExecutionSession hijacking, defacement
What Attacker AccessesServer files, databases, system commandsUser's browser session, cookies
SeverityCritical (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:

SSTI Audit Prompt
Review my codebase for Server-Side Template Injection (SSTI) vulnerabilities (CWE-1336): ## Check 1: Template String Construction Search for patterns where user input enters template strings: - Python: f"...{user_input}...", "..." + user_input, render_template_string() - Node.js: `...${userInput}...` with template engines, ejs.render() - PHP: "...$variable..." with Twig/Blade - Java: String concatenation into FreeMarker/Velocity templates Flag: Any user input concatenated into template strings ## Check 2: Dynamic Template Names Look for user-controlled template selection: - render(req.params.template) - render_template(user_input) - Template path derived from user input - Include statements with variable paths ## Check 3: Unsafe Helper Registration In Handlebars/custom engines, check for: - Helpers that execute shell commands - Helpers that access the filesystem - Helpers using eval() or exec() - Custom filters with dangerous operations ## Check 4: Template Engine Configuration Verify secure defaults: - Jinja2: sandbox mode for untrusted templates - EJS: avoid client option for untrusted data - Twig: sandbox policy enabled - FreeMarker: secure configuration ## Secure Patterns by Engine Jinja2 (Python/Flask): ```python # SECURE: Pass data as template variables return render_template('greeting.html', name=user_input) # Or with inline templates return render_template_string("Hello {{ name }}", name=user_input) ``` EJS (Node.js/Express): ```javascript // SECURE: Use data object, never concatenate res.render('page', {name: userInput}) // In template: <%= name %> for escaped output ``` Pug (Node.js): ```javascript // SECURE: Pass data object res.render('template', {name: userInput}) // In template: h1= name (interpolated and escaped) ``` Twig (PHP): ```php // SECURE: Use named template with data echo $twig->render('hello.twig', ['name' => $name]); ``` FreeMarker (Java): ```java // SECURE: Use data model template.process(dataModel, out); ``` For each vulnerability found: 1. Show the vulnerable code with line numbers 2. Demonstrate how {{7*7}} or ${7*7} would evaluate 3. Explain the RCE path for that specific engine 4. Provide secure replacement with data passed as variables 5. If render_template_string() is used, suggest switching to file-based templates

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