Server-Side Template Injection: Detection to RCE
SSTI from detection to impact: plaintext versus code context, engine identification, sandbox escapes, and why remote code execution is not a given.
If ${7*7} comes back as 49, your input may be executing inside the template that generates the response.
Server-Side Template Injection, SSTI, appears when user-controlled input becomes template code.
A template engine should receive data.
When it receives expressions, directives, or template-language code from the user, the trust boundary changes.
Depending on the engine and its sandbox, the result may include file reads, access to internal objects, or server-side code execution.
The examples below assume a lab or an authorized penetration test.
Template engines combine structure with data
Consider an email:
1
Hello {{ username }}
The application passes username as data.
The engine replaces the placeholder.
That is the intended flow.
Now consider unsafe code:
1
2
template = "Hello " + request.args["name"]
return render_template_string(template)
The user controls part of the template itself.
If the input is:
1
{{7*7}}
the engine may evaluate the expression.
That construction difference is the root of SSTI.
XSS and SSTI can look similar at first
Reflected input inside HTML often sends testers straight toward XSS.
Example:
1
Hello <b>Gabriel</b>
If the tag renders, XSS becomes a hypothesis.
Now send a template expression.
In Jinja2:
1
{{7*7}}
Response:
1
49
The calculation happened on the server before HTML reached the browser.
That changes the potential impact completely.
Do not identify the engine from one payload
Several engines share syntax.
{{7*7}} can work in more than one engine.
Another payload may behave differently.
PortSwigger uses a decision tree for this reason.
A good sequence detects evaluation first and identifies the engine afterward.
Errors help a lot.
A stack trace may reveal Jinja2.
Twig.
FreeMarker.
Velocity.
ERB.
Smarty.
When errors are not useful, compare engine-specific expressions.
Plaintext context is the easiest case
The user controls text inserted directly into the template.
Example:
1
Hello {{7*7}}
If the result is Hello 49, the evidence is clear.
At that point I stop using generic payloads.
I identify the engine.
I read its documentation.
I discover globals.
I inspect the sandbox.
I identify classes or helpers exposed by the application.
SSTI is highly engine-specific.
A payload copied from a blog often fails when version or context changes.
Code context requires understanding the expression
Sometimes user input lands inside a template expression already built by the developer.
Conceptual example:
1
engine.render("Hello {{ " + greeting + " }}", data)
A normal value may be:
1
user.name
Testing now depends on where that input sits inside the template grammar.
Characters closing the expression may show whether the input can escape its original context.
This resembles SQL Injection in one useful way.
Position inside the language changes which syntax works.
Do not assume RCE
SSTI often creates severe impact.
Some engines still do not expose enough primitives for operating-system command execution.
A sandbox may reduce reach.
A logic-less engine may be in use.
Dangerous objects may be unavailable.
The vulnerability still exists when the user controls template code.
Impact needs to match what the environment actually exposes.
Configuration read.
Secret exposure.
File read.
SSRF.
RCE.
Those are different levels.
Jinja2 shows the data-versus-expression problem clearly
Consider:
1
2
3
4
from flask import render_template_string
name = request.args.get("name")
return render_template_string("Hello " + name)
Request:
1
/?name={{7*7}}
Response:
1
Hello 49
That already proves evaluation.
In a lab, later steps can map objects available in the context.
I avoid jumping directly into an os.popen payload.
First determine what the engine actually exposes.
FreeMarker, Twig, and others use different syntax
FreeMarker has its own constructs.
Twig has its own filters and functions.
ERB executes Ruby.
Smarty uses another grammar.
That is why SSTI is more interesting than a payload list.
The vulnerability class is shared.
Exploitation needs to respect the actual language.
A sandbox is a barrier
Some template engines provide sandbox modes.
They restrict objects and methods.
Template sandboxes have seen bypasses over time.
Even without a bypass, user-controlled templates create a large surface.
If a product needs customization, I prefer a restricted language.
Mustache and other logic-less engines reduce expression capability.
They still require safe implementation.
Templates exist outside HTML
Email.
PDF.
Reports.
CMS content.
Notifications.
Invoices.
Document generators.
Marketing platforms.
Workflows.
Configuration that becomes a file.
Look anywhere a user can choose a template, layout, message, or model.
SSTI may live in an administrative feature and still be high impact when less-trusted users can reach it.
A method that avoids noise
Find reflection first.
Then send generic template syntax.
Look for evaluation or errors.
Identify the engine.
Read documentation.
Map objects and functions.
Demonstrate the smallest primitive that proves impact.
Remediation
Do not concatenate user input into templates.
Pass values as data.
Conceptual example:
1
return render_template("hello.html", name=name)
Template:
1
Hello {{ name }}
Now name is data.
If the product requires user-created templates, choose an engine with limited capability.
Apply external sandboxing.
Run rendering with minimal filesystem, network, and secret access.
Keep the engine updated.
Treat user-controlled templates as untrusted code.
HTML escaping does not fix SSTI
This confusion appears because template engines and XSS often sit close together in code.
HTML escaping protects output sent to the browser.
SSTI happens earlier during server-side template interpretation.
An expression may execute and the final result may still be perfectly escaped as HTML.
I therefore separate two questions.
Did the input become template syntax?
Was the resulting output safe for the browser?
Those are different stages.
Engine errors can be more useful than execution
During fingerprinting, I prefer controlled errors over aggressive payloads.
An exception may reveal the engine name, version, template path and loaded classes.
That narrows the search space quickly.
It may also reveal an application-specific wrapper around the engine.
That wrapper can expose dangerous helpers even when the raw engine would have a smaller surface.
Asynchronous rendering moves the impact
PDFs, emails and reports are often rendered by a separate worker.
The web request accepts the template and returns a job ID.
The SSTI executes seconds later inside another process.
Blind SSTI can appear in this shape.
Testing needs to inspect generated artifacts, logs, callbacks and side effects. A clean HTTP response does not end the hypothesis.
The impact may also change because the worker can have different filesystem access and credentials from the web server.
References
PortSwigger Web Security Academy, Server-side template injection.
PortSwigger Research, Server-Side Template Injection.
PortSwigger, Server-side template injection issue definition.


