Description
Server-side template injection (SSTI) occurs when user-controlled data is embedded into a template string that is subsequently evaluated by a server-side template engine such as Jinja2, Twig, Freemarker, Pebble, Velocity, Smarty, or Handlebars. Unlike cross-site scripting, where script execution targets the user's browser, SSTI executes in the server process itself — giving an attacker access to the full programming language runtime and, in most engines, a path to arbitrary OS command execution.
The vulnerability arises from a developer anti-pattern: building template strings dynamically from user input rather than passing user data as context variables to a fixed template. This is functionally equivalent to string-formatting a SQL query instead of using parameterized queries, and it falls squarely under OWASP A03:2021 (Injection) / CWE-94 (Improper Control of Generation of Code).
SSTI is particularly prevalent in Python web frameworks (Flask/Jinja2, Django), PHP applications (Twig, Smarty), Java frameworks (Freemarker, Velocity, Pebble), and Ruby (ERB). The impact varies by engine — Jinja2 and Freemarker offer particularly powerful expression evaluation that routinely leads to direct RCE.
How It Works
Detection begins by injecting a mathematical expression that the template engine would evaluate but a plain string would not:
{{7*7}} ← Jinja2, Twig
${7*7} ← Freemarker, Velocity
<%= 7*7 %> ← ERB
#{7*7} ← Ruby Pebble
If the response contains 49 instead of the literal string {{7*7}}, SSTI is confirmed.
A classic Jinja2 RCE payload uses Python's object hierarchy to reach subprocess:
{{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()[0].strip()}}
The attacker walks from a string object up to object, enumerates subclasses to find subprocess.Popen, and calls it with shell=True. A simpler modern payload using config or request context objects:
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
For Freemarker (Java), the freemarker.template.utility.Execute class is directly exploitable:
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
tplmap is the go-to automated tool for SSTI detection and exploitation, analogous to sqlmap for SQL injection:
python3 tplmap.py -u "https://target.com/profile?name=*" --os-shell
Impact
- Remote Code Execution — Full OS command execution as the web server process user
- Complete Server Compromise — Reading arbitrary files, writing web shells, installing backdoors
- Secrets Exfiltration — Accessing environment variables, config files, and in-memory credentials
- Internal Network Pivot — Using the compromised server to probe internal services
- Horizontal Escalation — In containerized environments, escaping to the container orchestrator if the process has elevated privileges
Detection
- Fuzz all reflected inputs — test any parameter where user input appears in the response. Inject
{{7*7}},${7*7}, and<%= 7*7 %>and check whether the response contains49. - Use polyglot probes — the string
${{<%[%'"}}%\triggers syntax errors in multiple engines; different error messages reveal the specific engine in use. - Run tplmap —
python3 tplmap.py -u "https://target.com/page?input=*"performs automated detection across Jinja2, Twig, Freemarker, Velocity, ERB, Smarty, and Pebble. - Inspect error messages — a
TemplateSyntaxError,UndefinedError, or Freemarker stack trace in the response confirms template context even before proving evaluation. - Test email and PDF generation features — these commonly use templating internally. Inject probes into name fields, subject lines, and document placeholders.
- Review source code — search for
render_template_string(user_input)in Flask,$twig->createTemplate($userInput)in PHP, andengine.evaluate(userControlledString)patterns in Java.
Remediation
Never pass user input as a template string. Always pass untrusted data as a context variable to a fixed, developer-controlled template:
# Vulnerable
return render_template_string(f"Hello {request.args['name']}")
# Safe
return render_template("hello.html", name=request.args['name'])
Use auto-escaping. Enable HTML auto-escaping in Jinja2, Twig, and other engines to limit cross-context injection even when variables are used safely.
Audit template rendering calls. In code review, flag any call that accepts a template string derived from user input. Enforce this with a Semgrep rule targeting render_template_string, Environment().from_string(), and equivalent APIs.
Apply engine-specific hardening. For Freemarker, set Configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER). For Twig, enable $twig->enableStrictVariables() and the sandbox extension.
Run in minimal-privilege containers. Contain blast radius — the web process should not have access to the database credentials file, the host network, or write access outside its working directory.
