Server-side template injection happens when user input is concatenated into a template string before that template is rendered, instead of being passed in as data the template engine merely fills in. The result is often worse than XSS on the same endpoint — because template engines are designed to execute logic, not just interpolate text, a successful SSTI payload frequently escalates to full remote code execution on the server.
The tell: a template string built with user input
The vulnerable pattern looks almost identical to a string-formatting bug: instead of render("welcome.html", {name}), the code does something like render_string("Hello " + name) or Template(f"Welcome {user_input}")— the user's value becomes part of the template source itself, not a variable the template references. Jinja2, Twig, Freemarker, Velocity, Handlebars, and EJS all support this pattern, and all of them will happily execute template syntax that arrives embedded in that string, including syntax the developer never intended to expose.
Where it tends to appear
- "Customizable" email or notification templates where an admin or end user can edit the template body — a legitimate feature that becomes a vulnerability the moment the same input path is reachable by a lower-trust user.
- Dynamic page or report generation where a title, header, or filename gets woven into a template string for convenience instead of passed as template context.
- Error pages or debug views that echo the requested path or query string back through a template renderer for a "friendly" message.
- Chat or support-ticket features that render user messages through a templating layer meant for markdown or variable substitution.
Why this is worse than it looks at first glance
A basic SSTI probe (something like {{7*7}} rendering as 49) looks harmless, which is exactly why it's dangerous — that same syntax space in most template engines can reach object introspection, and from there the engine's own Python/Java/JavaScript runtime, and from there arbitrary code execution. Confirming SSTI with a math expression and treating the finding as low severity because "it's just a template bug" is a common and costly misjudgment; in most engines the distance from confirmed injection to RCE is short.
The fix is separation, not sanitization
Sanitizing user input before it enters a template string is fragile, because the set of dangerous template syntax is large and engine-specific. The reliable fix is architectural: never build a template string from user input at all. Templates should be static, developer-authored files or strings; user input should only ever be passed in as context data that a template references, never as text that becomes part of the template's own source before compilation. Where a feature genuinely needs user-editable templates, isolate rendering in a sandboxed environment with a restricted subset of the engine's functionality, and treat that sandbox as a security boundary that needs its own review.
Checklist for reviewing a diff that renders templates
- No template string is built via concatenation or f-string interpolation of user input before being passed to the rendering function.
- User-facing "custom template" features render through a sandboxed or restricted template environment, not the same full-featured engine used for trusted, developer-authored templates.
- Error and debug views that echo request data through a templating layer are checked with the same scrutiny as any other user-input sink.
- A confirmed template-injection finding is treated as a potential RCE path, not downgraded until the actual blast radius in that specific engine has been verified.