Server-Side Template Injection: From Templates to RCE
How SSTI works: user input reaching template engines, detecting the flaw, sandbox escapes to remote code execution, and how to template safely.
Template engines exist to mix fixed layout with dynamic data. You write a template with placeholders, the engine fills the placeholders with values, and out comes a page or an email. The engine treats the template as code and the values as data. Server-side template injection, SSTI, is what happens when user input crosses that line and lands in the template as code, where the engine evaluates it on the server.
The reason SSTI is treated as a top-tier bug is the nature of template engines. They are not simple string replacers; they support expressions, object access, and method calls. Once an attacker's input is being evaluated by one, the path from a curious probe to running arbitrary commands on the server is often short.
The mistake that causes it
The safe way to use a template engine is to load a fixed template and pass user data into it as bound values. The engine keeps the template and the data separate.
The unsafe way is to build the template string itself out of user input, usually by concatenation. Consider a feature that personalises a greeting by inserting a name directly into the template source rather than into the rendering context. Whatever the user types becomes part of the template the engine compiles and evaluates. If they type template syntax, the engine runs it.
render("Hello " + userName) // userName is compiled as template code
render("Hello {{ name }}", {name}) // userName is passed as data, safe
The first line is the whole vulnerability. User input is being parsed as template language. The second passes the same input as a bound value, which the engine escapes and never executes.
SSTI is not XSS
The two are easy to confuse because both involve untrusted input reaching output, but they execute in different places and have different ceilings.
| Cross-site scripting | Server-side template injection | |
|---|---|---|
| Where code runs | Victim's browser | Your server |
| Language evaluated | HTML and JavaScript | The template engine's expression language |
| Worst case | Session theft, actions as the victim | Remote code execution on the server |
| Root cause | Unescaped output in the page | User input compiled as template code |
Because SSTI runs server-side, its worst case is not stealing a session; it is owning the host. On MITRE ATT&CK this fits exploitation of a public-facing application, T1190, when the injection reaches code execution.
How it is detected
Finding SSTI starts with a mathematical probe rather than an HTML one. If injecting a template expression that multiplies or adds two numbers causes the response to contain the computed result, the input is being evaluated by an engine. A response that echoes the literal text means it is being treated as data; a response that shows the arithmetic result means it is being executed.
From there the tester identifies which engine is in play, because syntax differs between them. Curly-brace expressions, percent tags, and dollar-brace forms point at different engines, and the follow-on payloads are engine-specific. Once the engine is known, the tester walks its object model to reach useful capabilities.
From expression to remote code execution
The escalation is a walk through the language's own object graph. Template expression languages can usually access an object's attributes and methods, and from a starting object an attacker can often navigate to base classes, then to imported modules, then to functions that read files or spawn processes. What begins as evaluating a small expression turns into calling the runtime's process or file APIs.
This is why SSTI is grouped with the most serious server-side flaws. The pattern of untrusted input driving server-side execution is shared with command injection and unsafe deserialization, and the defensive mindset carries across all three. See Command Injection Explained and Insecure Deserialization Explained for the neighbouring cases.
Several template engines ship a sandbox mode meant to restrict what expressions can reach. Treat it as hardening, never as the control that keeps you safe. These sandboxes have a long, public history of being escaped, because the expression language exposes enough of the object model to eventually reach a forbidden capability by an indirect route. If your safety depends on the sandbox holding, one published escape is full remote code execution. Keep untrusted input out of the template entirely.
Why filtering payloads does not work
Blocking specific characters or keywords in input is a losing effort here. Each engine has a rich syntax with many equivalent ways to express the same access, and the object graph offers many routes to the same dangerous function. Filtering the braces of one engine ignores the tags of another, and stripping one keyword leaves ten synonyms. As with the other injection classes, the attacker needs a single path the filter forgot.
The problem is not that the input contained bad characters. It is that user input was compiled as code at all. Fix that, and there is no payload to filter. For the broader discipline of handling untrusted input, see the Input Validation Guide.
How to defend against SSTI
Keep the template and the data on opposite sides of a line the attacker cannot cross.
- Never build templates from user input. Templates should be static assets defined by developers. User input enters only as bound data passed into the rendering context, never concatenated into the template source. This single rule eliminates the vulnerability.
- Pass untrusted data as context values. Use the engine's parameter binding so values are escaped and treated as data. If you find yourself constructing template strings at runtime from request data, stop and redesign.
- Prefer a logic-less template engine for untrusted content. Engines that support little or no expression evaluation give an attacker nothing to reach even if input leaks into a template. Choose these where users influence templated output, such as themes or notifications.
- Do not rely on the sandbox. If an engine offers a sandbox, enable it as one layer, but do not treat it as the boundary. Assume it can be escaped and put the real control on keeping input out of templates.
- Run the renderer with least privilege. Render in a low-privilege, isolated context so that if execution is reached, the blast radius is contained. Segmentation and dropped privileges limit what a successful escape can do.
- Validate and constrain any input that must influence templates. Where users genuinely need to affect layout, offer a fixed set of choices or a strict schema rather than free-form template text.
The one-line mental model for avoiding SSTI is to keep user input in the rendering context and out of the template source. A static template plus bound data is safe by construction, because the engine only ever executes code the developer wrote. The moment request data is concatenated into a template string, you have handed the attacker your server's expression language.
Where SSTI tends to appear
The bug follows a specific design temptation, so it clusters in predictable features. Knowing where to look shortens both testing and review.
The most common home is user-customisable output. Any feature that lets a user shape how something is rendered invites a developer to build a template from that user's input. Email and notification templates are a frequent example, where an administrator or user edits a message body that the server then renders with dynamic values. If that editable body is compiled as a template, the editor controls template code.
A second home is personalisation that inserts a value into a template source rather than into the rendering context. A greeting that puts a name directly into the template string, a subject line assembled by concatenation, or a document generated from a user-supplied layout all cross the line from data to code. The feature looks harmless because it only inserts a name or a title, and the mistake is that the insertion happens in the template rather than in the data passed to it.
A third home is any place that stores template fragments and later renders them with request data mixed in. Content management features, theme systems, report builders, and dashboard widgets often let users provide snippets. When those snippets are rendered by a full-featured engine, the snippet author has an expression language on the server.
The common signal in all three is the same design decision seen from different angles: the template string is being constructed from input that a user can influence. Wherever a feature description includes the words custom, template, theme, or layout, check whether the customisation lands in the template source or stays as bound data.
A worked example: from probe to object graph
Walk the tester's path from first suspicion to a foothold, staying at the level of mechanism so it applies across engines. A page reflects a value you control, say a name field, back into the response. The first question is whether that value is being rendered as data or evaluated as template code.
The probe is arithmetic, not markup. Submit an expression that an engine would evaluate to a number a plain string could not, for example a small multiplication written in the candidate engine's expression syntax. If the response contains the literal text you sent, the input is being treated as data and there is no injection at that point. If the response contains the computed product, the engine evaluated your input, and you have server-side template injection.
The next step is fingerprinting the engine, because every engine has its own syntax and its own escape chain. Different bracket styles, tag styles, and comment styles evaluate under one engine and error under another. Sending a small set of expressions and watching which produce results, which produce errors, and what those errors look like narrows the field to a specific engine.
With the engine known, exploitation becomes a walk through its object model. Expression languages typically let you read an object's attributes and call its methods. From an ordinary starting object, a tester navigates to its class, from the class to base classes, from there to loaded modules, and from a module to a function that reads files or spawns a process. Each hop uses only the language's legitimate reflection and attribute access. The endpoint is a call into the runtime's file or process interface, which is remote code execution. The important point is that no step is a trick the engine forbids; the engine is doing exactly what it was built to do, and the flaw was letting attacker input become the expression at all.
Detection signals: finding and confirming SSTI
SSTI hides among ordinary reflected values, so detection combines targeted probing with error and behaviour analysis.
- Arithmetic evaluation. The definitive signal is an expression that evaluates to a value a plain string cannot. If a template expression that multiplies two numbers returns their product in the response, the input reached an engine. This is the single most reliable probe.
- Engine-specific syntax responses. Send expressions in several engines' syntaxes and observe which evaluate and which are echoed. The pattern of what runs and what errors identifies the engine and confirms evaluation.
- Distinctive template errors. Malformed expressions often produce engine-specific error messages or stack traces. An error that names a template engine, a template compilation step, or an expression parser is strong evidence that input is being compiled as template code.
- Reflection points that differ from XSS behaviour. A value that renders inertly as HTML but transforms when given template syntax points at server-side evaluation rather than browser-side rendering. If braces or tags are being interpreted before the response leaves the server, that is the engine.
- Code review for the root cause. In source, search for template rendering calls whose template argument is built from request data by concatenation or interpolation, rather than a fixed template file with data passed as context. That construction is the vulnerability, and finding it in code is more certain than any black-box probe.
Confirming SSTI can tip immediately into code execution, because the same object walk that proves the bug is the path to running commands. When testing systems you are authorised to assess, stop at the least intrusive proof that settles the question, an arithmetic evaluation, rather than reaching for a process-spawning payload. The math probe proves evaluation without touching the host, which is all a finding needs.
SSTI among related server-side flaws
SSTI belongs to a family where untrusted input drives server-side execution. Placing it next to its neighbours clarifies what is specific to it.
| Flaw | What is confused | The interpreter | Root fix |
|---|---|---|---|
| Server-side template injection | Data treated as template code | The template engine | Keep templates static, pass input as context data |
| Command injection | Data treated as shell syntax | The system shell | Pass arguments to APIs, never build shell strings |
| SQL injection | Data treated as query syntax | The database | Parameterised queries, bound values |
| Insecure deserialization | Data treated as object state and behaviour | The deserializer | Do not deserialize untrusted data, or constrain types |
| Cross-site scripting | Data treated as page markup or script | The victim's browser | Contextual output encoding |
The unifying mistake across the server-side rows is the same: input that should be inert data is handed to an interpreter that treats it as instructions. SSTI is the case where the interpreter is a template engine, and because template engines expose expressions, attribute access, and method calls, the reachable ceiling is code execution on the server. The defensive instinct that works for all of them is to keep a firm boundary between the code the developer wrote and the data the user supplied.
Common misconceptions
SSTI is a kind of XSS. They share the shape of untrusted input reaching output, and they run in different places with different ceilings. XSS runs in the victim's browser; SSTI runs on your server, so its worst case is host compromise rather than session theft. Treating SSTI as merely an output-encoding problem misses that it is server-side code execution.
Escaping output prevents SSTI. Output encoding defends against XSS by neutralising markup in the rendered page. SSTI happens earlier, when the engine compiles attacker input as template code, before any output encoding applies. The fix is upstream: do not let input become the template.
The engine's sandbox makes it safe. Sandboxes are hardening with a long public history of being escaped, because the expression language exposes enough of the object model to reach a forbidden capability by an indirect route. A design whose safety depends on the sandbox holding is one published escape away from full compromise.
Filtering dangerous characters solves it. Each engine offers many equivalent ways to express the same access, and the object graph offers many routes to the same function. Blocking braces of one engine ignores the tags of another, and stripping a keyword leaves synonyms. The attacker needs one route the filter forgot.
Only certain engines are affected. Any engine that compiles a template from a string is exploitable when that string includes user input. The syntax and the escape chain differ by engine, and the underlying mistake, compiling untrusted input as a template, is engine-independent.
How it evolved
Template engines grew powerful to make dynamic pages and emails easy to build, adding expressions, filters, attribute access, and method calls. That power is what turns an injection into code execution. As applications increasingly let users influence templated output, through themes, email templates, dashboards, and notification formats, the temptation to build templates from user input grew, and with it the attack surface.
Security researchers formalised the class by showing that a simple evaluation probe reliably reveals it and that the escalation follows the engine's own object model to code execution. The methodology became standard: detect with an arithmetic probe, fingerprint the engine, then walk the object graph. Defensive guidance converged on a single durable rule, keep templates static and pass user input only as bound context data, because every attempt to filter payloads or lean on a sandbox eventually met a bypass. The lasting lesson mirrors the other injection classes: the fix is architectural, at the boundary between code and data, rather than a filter on the input.
Frequently asked questions
How is SSTI different from XSS in one sentence? XSS executes in the victim's browser and tops out at actions as the victim, while SSTI executes on your server and tops out at remote code execution on the host.
What is the safest way to confirm SSTI? An arithmetic evaluation probe. If a template expression that multiplies two numbers returns the product, input is being evaluated. It proves the bug without touching the host, which is the least intrusive confirmation.
Can I rely on the engine's sandbox mode? No, not as the boundary. Enable it as one layer of hardening, and assume it can be escaped, because these sandboxes have repeatedly been escaped. Put the real control on keeping untrusted input out of templates.
Why does character filtering not fix SSTI? Because the problem is that input was compiled as code at all, not that it contained particular characters. Engines offer many equivalent syntaxes and the object graph offers many routes, so a blocklist always leaves a path. Remove the compilation of untrusted input and there is nothing to filter.
What does the real fix look like in code? Load a fixed template defined by developers, and pass user input only as bound values into the rendering context, never concatenated into the template string. A static template plus bound data is safe by construction, because the engine only ever executes code the developer wrote.
Is a logic-less template engine worth using? Where users influence templated output, yes. An engine with little or no expression evaluation gives an attacker nothing to reach even if input leaks into a template, which shrinks the ceiling dramatically compared with a full-featured engine.
How do I limit damage if execution is reached anyway? Render in a low-privilege, isolated context so a successful escape has a small blast radius. Segmentation and dropped privileges do not prevent SSTI, and they contain what a foothold can reach, which matters when the primary control fails.
Where should I look first when reviewing for SSTI? Features described as custom, template, theme, layout, or notification, where user input might shape rendered output. In source, find rendering calls whose template argument is built from request data by concatenation. That construction is the vulnerability, and it is faster to spot in code than to find by probing.
Does SSTI only lead to code execution, or are there lesser impacts? Even without reaching code execution, an attacker who can evaluate expressions can often read server-side data, configuration, and objects reachable through the engine's context. That information disclosure is serious on its own, and it is usually a stepping stone toward the full object walk to execution.
Server-side template injection is what happens when a tool built to separate code from data is used in a way that merges them. The engine does exactly what it is designed to do, evaluate the template it is given, and the flaw is that the template was partly written by the attacker. Keep templates static, pass user input only as bound context data, choose a logic-less engine for untrusted content, and never lean on the sandbox alone. To see how the payloads that ride these bugs are structured, see our Exploit Intelligence dashboard, and for the wider category, the OWASP Top 10.
Related guides
Sources & further reading
Related guides
- Command Injection: Shell Metacharacters & Safe APIs
How OS command injection breaks out of an intended command via shell metacharacters, and why argument-array APIs beat string concatenation.
- Insecure Deserialization: Object Injection & Gadget Chains
Why deserializing untrusted data is dangerous, how gadget chains turn it into remote code execution, and safer serialization alternatives.
- Clickjacking Explained: UI Redress Attacks & Frame Defenses
How clickjacking hijacks a user's clicks with transparent iframes, what an attacker can trigger, and how to block framing with headers and CSP.