Application Security

Template Injection (SSTI) Prevention Guide

Server-Side Template Injection turns template engines into code execution engines. This guide covers SSTI in Jinja2, Twig, Freemarker, and other engines, with detection techniques and layered defenses.

Bob
Application Security Engineer
5 min read

Server-Side Template Injection is the vulnerability that turns your presentation layer into an attack surface. Template engines are designed to generate dynamic content by combining templates with data. When user input is embedded into the template itself rather than passed as data, the attacker gets to write code that the template engine executes.

The impact is almost always critical. Template engines in most languages provide access to the underlying runtime, which means SSTI typically leads to remote code execution. A vulnerability in how you render a user's profile name can end with the attacker running operating system commands on your server.

How SSTI Works

The difference between safe and vulnerable template usage is subtle:

Safe -- user input as data:

template = env.get_template("greeting.html")
# Template: "Hello, {{ name }}!"
output = template.render(name=user_input)

The user input fills a placeholder in a pre-defined template. The template engine treats it as a string value.

Vulnerable -- user input as template:

template = env.from_string("Hello, " + user_input + "!")
output = template.render()

The user input becomes part of the template itself. If the user supplies {{7*7}}, the output is "Hello, 49!" -- the template engine evaluated the expression.

That expression evaluation is the entry point. From there, the attacker escalates to arbitrary code execution by accessing the template engine's built-in objects and methods.

Exploitation by Template Engine

Jinja2 (Python). Jinja2 is the most commonly targeted engine because of Python's introspectable object model. The standard payload chain:

  1. Access a string object: ''
  2. Access its class: ''.__class__
  3. Walk the Method Resolution Order to object: ''.__class__.__mro__[1]
  4. List all subclasses: ''.__class__.__mro__[1].__subclasses__()
  5. Find a useful class (like subprocess.Popen) and call it

A typical RCE payload:

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

The subclass index varies by Python version, but the technique is reliable.

Twig (PHP). Twig is PHP's most popular template engine. Exploitation uses the _self object:

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

Or through the filter function:

{{'id'|filter('system')}}

Freemarker (Java). Freemarker provides built-in functions that enable code execution:

${"freemarker.template.utility.Execute"?new()("id")}

Or through the ObjectConstructor:

${object.getClass().forName("java.lang.Runtime").getRuntime().exec("id")}

Handlebars (JavaScript). While Handlebars is more restrictive than other engines, prototype pollution combined with custom helpers can lead to code execution:

{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('child_process').execSync('id');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

Detection Techniques

Manual detection. Inject template syntax and observe the output:

  1. Submit {{7*7}} -- if the response contains 49, the engine evaluates expressions
  2. Submit ${7*7} -- different engines use different delimiters
  3. Submit <%= 7*7 %> -- ERB (Ruby) syntax
  4. Submit #{7*7} -- Pug/Jade syntax
  5. Submit {7*7} -- Smarty (PHP) syntax

Polyglot payloads. Test multiple engines simultaneously:

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

This triggers errors or output differences in most template engines, confirming the injection point.

Automated detection. Tools like tplmap and Burp Suite's template injection scanner automate detection and exploitation across multiple engines.

Sandboxing and Its Limitations

Some template engines offer sandboxed modes that restrict available functions:

Jinja2 SandboxedEnvironment. Restricts attribute access and disallows calling dangerous methods. However, sandbox escapes are regularly discovered. The sandbox blocks direct access to __class__, but researchers find alternative paths through the object model.

Twig sandbox. Restricts available functions and filters to an allowlist. Effective when properly configured, but misconfiguration is common.

The fundamental problem: Sandboxes are blocklists. They try to prevent access to dangerous functionality while allowing everything else. Each new bypass demonstrates another path the sandbox did not anticipate. Sandboxes reduce risk but are not a complete defense.

Prevention Strategies

Separate user input from templates. This is the only reliable defense. User input should always be passed as data to a pre-defined template, never concatenated into the template string:

# SAFE: user input is data
template = env.get_template("profile.html")
output = template.render(name=user_input, bio=user_bio)

# VULNERABLE: user input is template
template = env.from_string(user_input)
output = template.render()

Use logic-less templates. Template engines like Mustache and Handlebars (without custom helpers) provide minimal logic capabilities, reducing the attack surface. If your templates do not need complex logic, use a simpler engine.

Input validation. If user input must appear near template syntax (e.g., in CMS systems), validate and sanitize aggressively. Strip or encode template delimiters ({{, }}, ${, <%). But this is a defense in depth measure, not a primary defense.

Template compilation. Pre-compile templates at build time, not at runtime. Pre-compiled templates cannot be modified by user input because the template is already parsed before user data arrives.

Content Security Policy. CSP does not prevent SSTI, but it limits the impact of SSTI-to-XSS chains by restricting script execution in the browser.

Principle of least privilege. Run the application with minimal OS permissions. Even if an attacker achieves RCE through SSTI, limited permissions constrain what they can do.

Common Vulnerable Patterns

Watch for these patterns during code review:

  • Custom email templates. Applications that let users design email templates often pass the template string through the engine.
  • CMS page builders. Content management systems that allow custom page layouts may evaluate user-controlled templates.
  • Report generators. Applications that generate reports from user-defined formats can be vulnerable if the format string is a template.
  • Error message customization. Applications that let administrators customize error messages and pass them through the template engine.
  • Internationalization. Translation strings that are processed through the template engine instead of a dedicated i18n library.

How Safeguard.sh Helps

Safeguard.sh monitors the template engines in your dependency tree for known SSTI vulnerabilities and sandbox escapes. Template engine CVEs are published regularly -- new bypass techniques for Jinja2, Twig, and Freemarker sandboxes appear every year. Safeguard.sh tracks these CVEs against your SBOM and alerts when your version of a template engine has a known escape, ensuring your team upgrades before attackers leverage the published bypass against your application.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.