Skip to content
العربية

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.

Estimated time
25 min
Level
Intermediate
Your progress in this track0%

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?

greet.js
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 secret and 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

safe.js
// 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.

Review questionTyping {{7*7}} into a name field returns 'Hello 49'. What does that tell you?

Labs

  • Your name becomes a template

    EasyBrowser lab

    Read the app secret config.secret by injecting a template expression.

  • It blocks one delimiter

    MediumBrowser lab

    Read config.secret again, bypassing the block on {{

  • It bans the word, forgets the object

    HardBrowser lab

    Read the secret without typing the word secret and without using {{