Server-Side Template Injection — Code in the Template
What Is a Template Engine?
Modern web applications rarely serve static HTML. Instead, they use template engines to merge dynamic data — user names, database results, configuration values — into HTML documents at request time. Popular template engines include:
| Engine | Language | Expression syntax |
|---|---|---|
| Jinja2 | Python (Flask, Django) | {{ expr }}, {% stmt %} |
| Twig | PHP (Symfony, Craft CMS) | {{ expr }}, {% stmt %} |
| Freemarker | Java (Spring) | ${expr}, <#stmt> |
| ERB | Ruby (Rails) | <%= expr %>, <% stmt %> |
| Velocity | Java | $variable, #directive |
| Pebble | Java | {{ expr }}, {% stmt %} |
The rendering pipeline works in two stages. First, the application assembles a template string containing static text interspersed with special syntax markers. Second, the engine evaluates the marked expressions and replaces them with computed values before the result is sent to the browser.
This two-stage pipeline is powerful but introduces a critical trust boundary: expressions inside a template are executed by the engine, not escaped as data. When that boundary is violated, server-side template injection (SSTI) occurs.
The Vulnerability: User Input as Template Code
SSTI arises when an application passes user-controlled input directly into the template string, rather than treating it as a safe data value to be substituted into an already-parsed template.
Vulnerable pattern (Flask / Jinja2)
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/welcome")
def welcome():
user_input = request.args.get("name", "")
# VULNERABLE: user_input is concatenated into the template string
return render_template_string("Welcome, " + user_input + "!")When user_input is {{ 7*7 }}, the template engine receives the string Welcome, {{ 7*7 }}! and evaluates the expression, returning Welcome, 49!. The attacker's input has become code.
Safe pattern
@app.route("/welcome")
def welcome():
user_input = request.args.get("name", "")
# SAFE: user_input is passed as a data variable, never part of the template string
return render_template_string("Welcome, {{ name }}!", name=user_input)Here, even if user_input contains {{ 7*7 }}, the engine treats it as a string value and renders Welcome, {{ 7*7 }}! literally — no evaluation, no injection.
The distinction is the same as parameterised SQL queries versus string concatenation in SQL injection: always separate code from data.
Real-World Examples
Uber SSTI (2016): A researcher discovered that Uber's partner portal reflected a user-supplied name field directly into a Jinja2 template. Injecting {{ config }} exposed internal configuration including database credentials. The bug was disclosed responsibly and earned a significant bounty.
Shopify Bug Bounty (2015): A Liquid template injection vulnerability in Shopify's theme editor allowed crafted template code to read server-side objects, demonstrating that even custom-built template engines can carry this class of vulnerability.
CVE-2019-8341 (Jinja2 SandboxedEnvironment bypass): Even the sandboxed environment in Jinja2 was found to be bypassable through Python's method resolution order (MRO) traversal, underscoring that sandboxing is a mitigation layer, not a complete fix.
The Escalation Path
SSTI vulnerabilities follow a predictable escalation chain:
- Probe — send
{{ 7*7 }}(Jinja2/Twig) or${7*7}(Freemarker). A response of49confirms the engine is evaluating expressions. - Enumerate the context — access built-in objects. In Flask/Jinja2,
{{ config }}dumps the entire application configuration dictionary. - Extract sensitive values —
{{ config['SECRET_KEY'] }}retrieves the Flask secret key, which is used to cryptographically sign session cookies. - Escalate to RCE — using Python's MRO traversal (
{{ ''.__class__.__mro__[1].__subclasses__() }}), an attacker can reach thesubprocess.Popenclass and execute arbitrary operating system commands.
Each step in this chain significantly increases impact: from information disclosure to full server compromise.
Lab Goal
In this lab you will work through steps 1 to 3 of the escalation chain using the interactive Jinja2 sandbox panel on the right:
- Confirm the injection point by evaluating a mathematical expression.
- Access the Flask
configobject to enumerate application settings. - Extract the
SECRET_KEYvalue from the configuration dictionary.
Understanding this chain — and why each step is dangerous — is the foundation for recognising and preventing SSTI in production applications.