What is Server Side Template Injection? SSTI Explained

What is Server Side Template Injection? SSTI Explained

Many web apps build pages by dropping data into a template before sending it to the browser. Server side template injection happens when user input reaches the template engine as template code instead of plain data, so the server runs whatever the attacker writes. What starts as a string in a form field can end up reading server configuration, files, and in the worst case running arbitrary commands.

How server side template injection works

Picture a small app called Acme Notes. It lets people set a display name, and the welcome banner greets them by it. The developer wanted a quick way to personalize the message, so they built the banner by stuffing the name straight into a template string:

# Acme Notes, Python with Jinja2 (vulnerable)
name = request.args.get("name")
template = "Hello " + name + ", welcome back to Acme Notes"
return render_template_string(template)

The mistake is concatenating user input into the template source. Jinja2 now treats the name as template code, not as a value to display. So if a visitor sets their name to {{7*7}}, the engine evaluates the expression and the banner reads Hello 49, welcome back to Acme Notes. A normal user would never see 49 there. That stray math is the tell that the input is being executed.

The app meant to print the user’s text. Instead it is running the user’s text. That gap between data and code is the whole bug.

From {{7*7}} to reading config and RCE

Returning 49 is harmless on its own. The reason this bug matters is what comes next. Template engines expose objects to the templates they render, and an attacker who controls template code can walk those objects to reach far more than a greeting.

In a Jinja2 app, a common next probe is to print the application config. The attacker sets their name to {{config}} and the banner dumps the Flask config object, which often holds secret keys, database URLs, and API tokens:

# Input
name = {{config}}

# Output (illustrative)
<Config {'SECRET_KEY': 'a8f3...', 'SQLALCHEMY_DATABASE_URI': 'postgres://...'}>

From there, the escalation is object traversal. Python objects expose their class, base classes, and subclasses through attributes like __class__ and __mro__. By climbing from a harmless string to object and back down to a subclass that can run system commands, an attacker reaches code execution. The exact chain is engine specific and we are not publishing a working one here, but the shape looks like this:

# Shape of the escalation, not a copy paste payload
{{ ''.__class__.__mro__[1].__subclasses__() }}   # enumerate reachable classes
# ... then pick a class that wraps os/subprocess and call it

That is the path from a math test to remote code execution. The same idea applies across engines. Each one exposes a different object graph, so the traversal differs, but the principle holds: control the template, reach the runtime.

Client side vs server side template injection

The names sound alike and they are easy to confuse, so it helps to separate them.

  • Server side template injection runs on the server, inside the rendering engine. The impact is server config disclosure, file reads, and remote code execution. This is the dangerous one.
  • Client side template injection runs in the browser, inside a frontend framework template such as an older AngularJS expression context. The impact is usually closer to cross site scripting, contained in the visitor’s session, not on your server.

A quick way to tell them apart: if {{7*7}} resolves to 49 in the raw HTML returned by the server before any JavaScript runs, you are looking at server side injection. Both are forms of injection, the same family as command injection, where untrusted input crosses into an interpreter.

How to detect it

Detection rests on a small set of probes against an app you own or are authorized to test.

The {{7*7}} test

Send a math expression in each input that ends up rendered: {{7*7}}, and for other engines ${7*7} or <%= 7*7 %>. If the response contains 49 instead of the literal text, the input is being evaluated.

A polyglot probe

You often do not know which engine is in use. A single probe that mixes several syntaxes lets one request fan out across engines. A common one looks like ${{<%[%'"}}%\, which is malformed in most contexts and tends to trigger a revealing error or a partial evaluation that names the engine.

Error based clues

Even when nothing evaluates, a broken template expression often throws a stack trace. The exception class and file paths usually name the engine outright, for example a jinja2.exceptions.TemplateSyntaxError or a Freemarker parse error. That tells you what to test next.

The engine families you will meet

You do not need to memorize every engine, but knowing the major families and their tells speeds up both detection and fixing. At a high level:

  • Jinja2 (Python, used by Flask). Syntax {{ ... }}. The {{config}} dump and class traversal live here.
  • Twig (PHP). Also {{ ... }}, with filters like {{7*7}} evaluating to 49. Object access differs from Jinja2.
  • Freemarker (Java). Syntax ${ ... }, with built in helpers that can reach Java’s runtime if left unsandboxed.
  • ERB (Ruby). Syntax <%= ... %>, which embeds raw Ruby, so injection here is direct code execution.

The lesson across all of them: a feature meant to format output becomes an execution surface the moment user input controls the template rather than fills it.

How to prevent server side template injection

The fixes are concrete and most of them are about keeping data and code apart.

  • Never pass user input into a template as template code. The Acme Notes bug came from concatenating the name into the template source. Pass it as a context variable instead, so the engine treats it as data: render_template("banner.html", name=name), never render_template_string("Hello " + name).
  • Use logic less or sandboxed templates. Engines like Mustache or Handlebars are logic less by design, so there is no expression to inject into. When you must use a richer engine, run it in its sandboxed mode so object traversal is blocked.
  • Apply contextual escaping. Make sure output is escaped for the context it lands in, HTML, attribute, or JavaScript, so injected markup is rendered as text, not interpreted.
  • Use allowlists for any dynamic template choice. If users can pick a template or a theme, map their choice to a fixed set of known names on the server. Never build a template path or template body from raw input.
  • Keep engines patched and review the render calls. Search your code for the engine’s render from string functions. Those are where this bug almost always hides.

For the wider pattern, our injection and input category covers the family this belongs to, and the web security glossary defines the terms used above.

Why this rewards understanding the app

You rarely find server side template injection by firing a fixed payload list. You find it by noticing that a field is reflected, asking whether that reflection passes through a template, and testing that assumption with a single {{7*7}}. The bug is an assumption the developer made, that the name was only ever data, and the way to catch it is to question that assumption directly.

That is the kind of bug an autonomous researcher that tests an app’s assumptions is built to surface. You can read more about that approach on our about page.

Frequently asked questions

What is server side template injection?

It is a bug where user input reaches a template engine as template code instead of plain data, so the server executes it. In a vulnerable app, setting a field to {{7*7}} returns 49 because the engine evaluated the expression. From there an attacker can read server config and often reach remote code execution. The formal bug class is described in MITRE CWE 1336.

How is the {{7*7}} test used to detect it?

You send a math expression into each input that gets rendered, such as {{7*7}} for Jinja2 or Twig, ${7*7} for Freemarker, and <%= 7*7 %> for ERB. If the response shows 49 instead of the literal text, the input is being evaluated as template code rather than displayed, which confirms server side template injection. A normal app would echo the characters unchanged.

What is the difference between server side and client side template injection?

Server side template injection runs inside the rendering engine on the server, so the impact is config disclosure, file reads, and remote code execution. Client side template injection runs in a browser framework template and behaves more like cross site scripting, contained in the visitor’s session. If {{7*7}} resolves to 49 in the raw HTML before any JavaScript runs, it is server side. See the OWASP guide to server side template injection.

How do you prevent server side template injection?

Never concatenate user input into a template body. Pass it as a context variable so the engine treats it as data, for example render_template("banner.html", name=name) rather than building the template string from input. Use logic less templates like Mustache or run richer engines in sandboxed mode, apply contextual output escaping, and map any user chosen template to a fixed allowlist of known names on the server.