Server-Side Template Injection (SSTI) happens when an application places untrusted input into a server-side template that is then rendered, so the input is evaluated as template syntax rather than plain data. Depending on the engine, Jinja2, Twig, Freemarker, Velocity, or others, this leads to information disclosure and very often full remote code execution. It is usually introduced when developers concatenate user input into a template string instead of passing it as a bound variable.
What SSTI is
Template engines let developers build HTML by mixing static markup with dynamic expressions, for example Hello {{ name }}. The engine evaluates anything inside its delimiters. SSTI occurs when user input reaches the template string itself rather than being passed in as a bound variable.
The difference is subtle but critical:
- Safe:
render_template('hi.html', name=user_input)passes the input as data. - Vulnerable:
render_template_string('Hello ' + user_input)concatenates the input into the template, so the engine evaluates it.
Once input is evaluated as template code, the attacker has a foothold into the engine's object model, and on most engines that path leads to the underlying operating system.
Detection and the abuse
Testers first confirm evaluation with a math probe, then identify the engine, then reach the OS. Shown for defensive context:
- Detect: submit
${7*7},{{7*7}}, or<%= 7*7 %>. A response containing49confirms server-side evaluation. - Fingerprint the engine: engine-specific payloads behave differently, for example
{{7*'7'}}returns7777777in Jinja2 but49in Twig. - Reach code execution (Jinja2 example):
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}walks the object model to theosmodule and runs a command. - Twig example:
{{ ['id']|filter('system') }}invokes a PHP callback.
Because SSTI so reliably becomes command execution, it is one of the highest-severity findings surfaced during web application penetration testing.
How to defend
The root cause is treating user input as template code, so the fix is to make that impossible:
- Never concatenate user input into a template string. Always pass it as a bound variable or context value.
- Prefer logic-less templates (for example Mustache) where the engine cannot evaluate arbitrary expressions.
- Sandbox the engine if dynamic templates are unavoidable, and keep the sandbox patched, since many sandbox escapes are known.
- Validate and constrain input that must appear in a template context, and encode output for its destination.
SSTI is a design-level flaw, so review anywhere a template is built from a string rather than a fixed file.