SecureBlock
Schwachstellen-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 tplmap — python3 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.

Bereit, wenn Sie es sind
Definieren Sie einen Pentest-Umfang in den nächsten zwei Minuten.
Jetzt Umfang definieren