SSTI (Server-Side Template Injection)
How putting user input into a template string lets you run template expressions on the server, why syntax and word blacklists fail, and how to pass input as data instead.
- Track
- Web security
- Estimated time
- 25 min
- Level
- Intermediate
Before this lesson
A template engine turns a template like Hello {{ name }} plus some data into finished text. SSTI happens when the user’s input becomes part of the template text rather than the data passed to it. Then {{ ... }} you type is evaluated by the engine on the server — reading variables, dumping objects, and in real engines reaching code execution.
What is the vulnerability?
engine.render('Hello ' + req.body.name, { config });The name is concatenated into the template. Type {{7*7}} and the page says Hello 49 — the engine evaluated your expression. That 49 is the proof: your input is code, not text. From there, {{config.secret}} reads whatever the template context holds.
Why does it happen?
The engine is meant to evaluate the template and only substitute the data. By building the template out of user input, the application hands the user the engine’s full expression power. It is the same mistake as SQL injection and command injection, one layer up: data placed where code is expected.
Why blacklists fail
- Block a syntax. Ban
{{and many engines still evaluate${ ... }. Blocking one delimiter leaves the others. - Ban a word. Block the word
secretand you reach it another way — Reading the value through the object that contains it, instead of naming it directly.${config}prints every value, the secret included, without the banned word.
You bypass both — the {{ block and the secret ban — in the medium and hard labs.
How to prevent it
// A FIXED template; the input is passed as DATA and is only ever substituted, never evaluated.
engine.render('Hello {{ name }}', { name: req.body.name });Never build a template from user input — keep templates static and pass user values as data. Do not place secrets in the template context. If you truly must render user-provided templates, use a sandboxed engine with no access to objects or builtins, run it with least privilege, and treat it as code review, not a text feature.
Labs
Your name becomes a template
SolvedEasyBrowser lab
Read the app secret config.secret by injecting a template expression.
Enter the labLockedIt blocks one delimiter
SolvedMediumBrowser lab
Read config.secret again, bypassing the block on {{
Enter the labLockedIt bans the word, forgets the object
SolvedHardBrowser lab
Read the secret without typing the word secret and without using {{
Enter the labLocked
