Blog · E.15 · Technical

Server-side template injection explained

Template engines exist to execute expressions — that's their job. So when user input slips into a template as code instead of data, the attacker inherits that execution. A single {{7*7}} that comes back as 49 is the tell, and from there it's often a short climb to running commands on your server. Here's how SSTI works, the detect-then-escalate method, which engines are affected, and how to design it out.

SSTITemplate InjectionRCEJinja2 / TwigWeb AppSec
SSTI: Input Evaluated as Template Code · Detect with {{7*7}} · Escalate to RCE · Jinja2 / Twig / Freemarker / Velocity · Fix = Pass Data, Not Templates SSTI: Input Evaluated as Template Code · Detect with {{7*7}} · Escalate to RCE · Jinja2 / Twig / Freemarker / Velocity · Fix = Pass Data, Not Templates
// TL;DR

Server-side template injection (SSTI) happens when user input is embedded into a server-side template and evaluated by the engine as code rather than treated as data. Because engines like Jinja2, Twig, Freemarker and Velocity execute expressions, injected input can run attacker expressions on the server — escalating to remote code execution where the sandbox is weak or absent. Detection is detect-then-escalate: inject a maths expression (e.g. {{7*7}}); if it returns 49, SSTI is present — then fingerprint the engine and escalate. It's a manual finding scanners often miss. The fix: never concatenate user input into templates — pass data as parameters into fixed templates; sandbox and isolate if user templates are unavoidable. Full explainer below; SSTI is tested in every web app engagement.

// 01 What SSTI is

Modern apps render dynamic pages with template engines — you write a template with placeholders, and the engine fills in values and evaluates expressions. That expression-evaluation is the feature and the danger. SSTI occurs when user input is embedded into a template and evaluated by the engine instead of being handled as inert data. The usual cause is developers building templates by concatenating user input — for example, dropping a user-supplied name straight into the template string — rather than passing that name as a parameter to a fixed template. Once the input is part of the template source, the engine treats it as code, and the attacker can write expressions the engine will dutifully execute. It's the same root cause as other injection bugs: data crossing into a code context.

// 02 Detect, then escalate

SSTI testing follows a disciplined two-step method. Step one — detect: into every reflected input, inject a simple mathematical expression in template syntax, classically {{7*7}} (and variants for different engines). If the response contains 49 rather than the literal {{7*7}}, the input is being evaluated — SSTI confirmed. Step two — fingerprint: different engines share similar syntax but diverge in the details, so the tester sends engine-specific probes to identify exactly which engine is running (Jinja2 vs Twig vs Freemarker, etc.), because the path to code execution is engine-specific. Only then do they escalate. This iterative, syntax-aware process is why SSTI is a hallmark of skilled manual testing — a scanner may flag reflected input but rarely confirms evaluation or safely proves the escalation.

// 03 From maths to remote code execution

Evaluating 7*7 is harmless; the point is what it proves. Once the attacker can run expressions, they use the template engine's own language features to climb out of the template context toward the underlying runtime — reaching built-in objects, classes and methods that let them read files or invoke system commands. Each engine has well-documented escalation techniques for this journey from “evaluate an expression” to “execute a command.” Where the engine has no effective sandbox, or the sandbox can be escaped, the outcome is arbitrary code execution. And because templates render with the application's own privileges, that typically means full server compromise — the same critical-severity endpoint as insecure deserialization, reached by a different road.

// 04 Which engines & where it appears

01

Python

Jinja2, Mako — common in Flask/Django-adjacent stacks; well-documented RCE paths.

02

PHP

Twig, Smarty — widely used; escalation depends on sandbox configuration.

03

Java

Freemarker, Velocity, Thymeleaf — powerful engines with strong RCE potential.

04

Common sinks

Email/notification templates, custom report builders, CMS themes, anything letting users shape output.

SSTI concentrates wherever an app lets users influence generated content — customisable email templates, report and invoice builders, CMS theming, and “preview your message” features. Those are exactly the places a template gets built from user-tainted strings.

// 05 How to fix it

The definitive fix is a design rule: never build templates by concatenating user input. Always pass user data as parameters into a fixed, pre-defined template, so the engine treats it as data, not code — this removes the vulnerability entirely. If your product genuinely must let users supply templates (some do — email builders, theming), then use a logic-less or heavily sandboxed engine designed for untrusted input, and render in a low-privilege, isolated environment as defence in depth so that even a sandbox escape is contained. Keep template libraries patched, since new sandbox-escape techniques surface over time. As with XXE and deserialization, safe-by-design usage beats filtering dangerous syntax — the syntax space is too large to blocklist reliably. SSTI is a standard, high-priority check in every web app test we run, per our methodology.

// 06 Frequently asked questions

What is server-side template injection?

SSTI occurs when user input is embedded into a server-side template and evaluated by the engine as code rather than data. Engines like Jinja2, Twig, Freemarker and Velocity execute expressions, so injected input can run attacker expressions on the server — ranging from information disclosure to full RCE. It typically arises when developers concatenate user input into templates instead of passing it as data.

How does SSTI lead to RCE?

Once an attacker can inject expressions, they use the engine's language features to navigate from the template context to the runtime — reaching built-in objects, classes and methods that read files or run commands. Each engine has documented escalation techniques; where the sandbox is weak or escapable, the result is arbitrary code execution. Templates run with the app's privileges, so SSTI often means full compromise.

How do you detect SSTI?

Detect-then-escalate. Inject a maths expression in template syntax (e.g. {{7*7}}) into each reflected input; if the server returns the computed result (49) rather than the literal text, SSTI is present. Then fingerprint the engine, because escalation differs per engine. It's a manual, iterative process requiring knowledge of template syntax, which is why scanners often miss it.

How do you prevent SSTI?

Never concatenate user input into templates — pass user data as parameters into a fixed template so it's treated as data. If users must supply templates, use a logic-less or heavily sandboxed engine and render in a low-privilege, isolated environment. Keep libraries patched. Safe-by-design usage beats filtering dangerous syntax.

// 07 Related reading

UG

Usama Gul

Founder & Penetration Testing Lead, CyberFortify

Chases SSTI from a single reflected {{7*7}} through engine fingerprinting to a proven, controlled RCE — the manual escalation scanners can't perform.

User input shaping your output?

Email builders, report generators, CMS themes — anywhere users influence generated content can be SSTI-vulnerable. We detect it, safely prove the RCE path, and hand you the design fix.

Scope a web app test → Web app testing →