Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Academy Server-Side Template Injection (SSTI)
Advanced · 20 min

Server-Side Template Injection (SSTI)

Learn how user input injected into templates can lead to remote code execution — and why you should never render user data as template syntax.

1 How SSTI Works

Server-Side Template Injection (SSTI) occurs when user-supplied input is embedded directly into a template string that is then rendered by the template engine. Instead of being treated as data, the input is interpreted as template syntax — allowing an attacker to run arbitrary expressions.

Vulnerable Flask/Jinja2 example:

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'World')
    # DANGER: user input is part of the template string itself
    template = f"<h1>Hello, {name}!</h1>"
    return render_template_string(template)

An attacker sends ?name={{7*7}}. The server renders <h1>Hello, 49!</h1> — proving that Jinja2 evaluated the expression. This is the classic detection payload.

Template engines that are vulnerable when fed user input:

  • Python: Jinja2, Mako, Chameleon
  • PHP: Smarty, Twig, Blade
  • Java: Freemarker, Velocity, Thymeleaf
  • JavaScript: Pug, Handlebars, EJS
  • Ruby: ERB

Detection payloads vary by engine. {{7*7}} = 49 in Jinja2/Twig; 49 = 49 in Freemarker; #{7*7} = 49 in Ruby ERB.

2 Jinja2 Attack Chains to RCE

Jinja2 SSTI can escalate to Remote Code Execution (RCE) via Python's object introspection system. Every Python object has a __class__, which has __mro__ (method resolution order), leading to every class loaded in the process.

Read application config (information disclosure):

{{config}}
{{config.items()}}
{{config['SECRET_KEY']}}

RCE via __mro__ chain to os.popen (simplified):

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

Where X is the index of the subprocess.Popen class in the subclass list. The attacker iterates through subclasses to find useful ones (Popen, os._wrap_close, etc.).

Twig (PHP) SSTI to RCE:

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

Why this is devastating: A single SSTI vulnerability in a template string renders the entire server compromisable — attackers can read files, exfiltrate environment variables (API keys, database passwords), spawn reverse shells, and pivot to internal services.

3 Safe Alternatives — Never Render User Input as Templates

The fundamental rule: user-supplied data must be passed as a context variable to a pre-compiled template, never embedded in the template string itself.

The safe pattern with render_template:

from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'World')
    # SAFE: name is data passed to a fixed template, not part of the template
    return render_template('greet.html', name=name)

The greet.html file:

<h1>Hello, {{ name }}!</h1>

Jinja2 auto-escapes variables in HTML templates, so {{7*7}} is rendered as the literal string {{7*7}}, not evaluated.

If you must use render_template_string, never include user data in the template string. Pass it as a keyword argument:

# Still OK — user input is a context variable, not part of the template syntax
return render_template_string("<h1>Hello, {{ name }}!</h1>", name=name)

Sandboxing: Jinja2 provides a SandboxedEnvironment that restricts access to dangerous attributes. It is an additional defense layer but is not a substitute for correct template design — determined attackers have escaped Jinja2 sandboxes before.

Additional mitigations: Disable debug mode in production (prevents detailed error messages that aid enumeration), run the application with minimal OS privileges, and apply WAF rules that block {{, __class__, __mro__ in user inputs.

Knowledge Check

0/4 correct
Q1

An attacker sends ?name={{7*7}} to a web app and receives "Hello, 49!" in the response. What does this confirm?

Q2

How can a Jinja2 SSTI vulnerability lead to Remote Code Execution?

Q3

What is the correct fix for render_template_string(f"Hello {name}") where name comes from user input?

Q4

Which Jinja2 payload is most likely to reveal sensitive server configuration like the SECRET_KEY?

Code Exercise

Fix the SSTI-Vulnerable Template Rendering

The Flask route below uses render_template_string() with user input directly in the template string — a critical SSTI vulnerability. Fix it by using render_template() with a template file, or by keeping render_template_string() but passing the user data as a context variable instead of embedding it in the string.

python