SecureBlockLog inStart a pentest
Vulnerability Repository
CriticalInjection

Server-Side Template Injection (SSTI)

User input rendered directly inside a server-side template engine executes arbitrary code, typically leading to full remote code execution on the host.

CVSS 9.0CWE CWE-94OWASP A03:2021 — Injection

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

  1. 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 contains 49.
  2. Use polyglot probes — the string ${{<%[%'"}}%\ triggers syntax errors in multiple engines; different error messages reveal the specific engine in use.
  3. Run tplmappython3 tplmap.py -u "https://target.com/page?input=*" performs automated detection across Jinja2, Twig, Freemarker, Velocity, ERB, Smarty, and Pebble.
  4. Inspect error messages — a TemplateSyntaxError, UndefinedError, or Freemarker stack trace in the response confirms template context even before proving evaluation.
  5. Test email and PDF generation features — these commonly use templating internally. Inject probes into name fields, subject lines, and document placeholders.
  6. Review source code — search for render_template_string(user_input) in Flask, $twig->createTemplate($userInput) in PHP, and engine.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.

Ready when you are
Scope a pentest in the next two minutes.
Start scoping