Lesson 7 · Reading · 30 min
Server-side template injection
Web apps build pages from templates: a template has fixed text with placeholders, and the engine fills the placeholders with data. The safe way passes your data into a placeholder. The dangerous way puts your input into the template itself, so the engine treats it as an expression to evaluate, and template expressions can often reach objects and functions that run code. This is Server-Side Template Injection, SSTI.
The tell: math that gets done
The signature test for SSTI is beautifully simple. Template engines evaluate expressions inside their delimiters, so if your input is being placed into a template, a mathematical expression will be calculated instead of shown literally. Send:
{{7*7}}
and if the page comes back showing 49, the engine evaluated your expression, your input is in the template, not just filling it. (Different engines use different delimiters: {{ }}, ${ }, #{ }, <%= %>. If {{7*7}} shows literally, try the others.) 49 where you wrote 7*7 is SSTI, confirmed.
Where it hides
SSTI shows up wherever user input flows into a server-side template, which is more places than you would think:
- Anything that renders your input back through the templating layer: a customisable email or notification ("Hi {{name}}"), a template a user can edit, a report or document footer/header you supply, a profile field rendered through the engine.
- Error pages and search results that build the response with a template and interpolate your input into it.
- "White-label"/theming features where users provide template snippets, the most direct case.
The clue that separates SSTI from plain XSS: with XSS your <b> renders as HTML; with SSTI your {{7*7}} gets computed. If math is evaluated, you are in the template engine, and that is a server-side bug, not a client-side one.
From 49 to code execution
Confirming SSTI is step one; the impact is that most engines' expression languages can reach the underlying language's objects, and from there the file system or a shell. The escalation is engine-specific, so first identify the engine (the syntax that works, and probes designed to error differently per engine tell you whether it is Jinja2, Twig, Freemarker, Velocity, ERB, and so on). Then use that engine's known route from an expression up to its runtime, walking object attributes to reach a class that can run OS commands. The details differ per engine and are well documented (PortSwigger and the "PayloadsAllTheThings" SSTI notes catalogue them); the principle is constant: an evaluated expression is a foothold, and template languages usually expose enough to climb from arithmetic to command execution.
Some engines run in a sandbox meant to prevent exactly this. Sandboxes are frequently escapable, a missed attribute, an allowed function that reaches a dangerous one, so "it is sandboxed" is a claim to test, not a guarantee.
The honesty gate
The finding ranges from information disclosure (reading server-side variables/config through the template) up to full RCE. Prove the level you actually reached: {{7*7}} = 49 proves injection; reading a server variable proves disclosure; running id proves execution, claim only what you demonstrated. Prove execution with a read-only command and stop; do not run destructive payloads or pivot on a real system. On this Academy's labs, escalate all the way.
Your labs
NoteForge greeting template RCE is the direct case: a greeting/template feature where your input is evaluated, confirm with {{7*7}}, identify the engine, and climb to command execution. ReportCraft footer sandbox escape is the harder, realistic one: the template engine runs in a sandbox meant to stop you, and the lab is about escaping it. Do NoteForge first to learn the escalation, then ReportCraft to learn that a sandbox is a challenge, not a wall.
Where this goes next
SSTI evaluated an expression you supplied. The last lesson of this path abuses a different trust in your data: insecure deserialization, where the app rebuilds an object from bytes you control and, in doing so, triggers a chain of existing code that ends in compromise, plus how to handle a target running a library with a known, published vulnerability.
This lesson has 3 labs to try what you just read.