What Is Server-Side Template Injection?

Template engines like Jinja2, Twig, Freemarker, and Velocity allow developers to embed expressions in templates β€” {{ user.name }} outputs the user's name. These expressions are evaluated server-side with full access to the application context.

SSTI occurs when user-controlled input is rendered directly by the template engine rather than being passed as a safe variable. The distinction is critical:

jinja2_example.py Python
from jinja2 import Environment

env = Environment()

# SAFE: user input is a variable value β€” not rendered as template
template = env.from_string("Hello, !")
template.render(name=user_input)  # user_input is data

# VULNERABLE: user input is the template string itself
template = env.from_string(f"Hello, {user_input}!")
template.render()  # user_input is rendered as template code

Impact: In Jinja2, SSTI with server-side evaluation commonly leads to full RCE via Python's __class__.__mro__ chain. Freemarker SSTI can execute arbitrary Java code. This is a critical severity finding in virtually every penetration test.

Jinja2 SSTI

Jinja2 is used in Flask, Django (with Jinja2 backend), Ansible, and many Python applications. The detection probe is simple: if the input {{7*7}} produces 49 in the response, the application is vulnerable.

ssti-probe.txt Probes
# Detection probe β€” renders to 49 if vulnerable
{{7*7}}

# Config object exposure
{{config}}

# Environment access via class chain
{{"".__class__.__mro__[1].__subclasses__()}}

Twig SSTI (PHP)

Twig is the default template engine for Symfony and many PHP frameworks. Detection probe: {{7*7}} renders to 49. Twig's sandbox mode limits available functions, but misconfigured sandboxes can still be escaped.

Freemarker (Java): Detection probe is ${7*7}. Exploitable via <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} β€” this directly calls Runtime.exec() in Java. Freemarker SSTI is classified as critical in every CVSS scoring.

Detection Techniques

  • Arithmetic probes: {{7*7}}, ${7*7}, #{7*7} β€” if the response contains 49, template is rendering user input
  • Engine fingerprinting: Different engines use different syntax. {{7*'7'}} returns 49 in Twig but 7777777 in Jinja2 β€” tells you which engine is in use
  • tplmap: Automated SSTI detection and exploitation tool, similar to sqlmap but for template injection
  • SAST: Search for render_template_string, Environment().from_string, or any template rendering function that accepts user input as the template string

Prevention

safe_template.py Python (Jinja2)
from jinja2 import Environment, select_autoescape

env = Environment(autoescape=select_autoescape(["html"]))

# CORRECT: template string is from your codebase, not user input
# User input only appears as variable values
template = env.from_string("Hello, ! You searched for: ")
output = template.render(
    name=user.name,      # safe variable
    query=user_query    # safe variable β€” not template code
)

The rule: the template string must come from your code or a trusted configuration store β€” never from user input. User input goes only into template variables, where it is treated as data, not code.

If you genuinely need user-defined templates (e.g., email template customisation), use a logic-less template engine like Mustache or Handlebars, which have no expression evaluation capability.

Detect SSTI in Your Codebase

AquilaX SAST detects user input passed to template rendering functions β€” the root cause of server-side template injection vulnerabilities.

Start Free Scan