Server-Side Template Injection (SSTI): Detect and Identify Template Engines

Tested on Kali GNU/Linux Rolling 2026.2 (kali-rolling)
Package python3-flask (Jinja2 via Flask); browser or curl for probes
Lab environment Pentest lab
Cert prep CompTIA PenTest+ PT0-003: injection classes beyond SQLi and XSS
Privilege Normal user to run the lab app and send requests
Scope What SSTI is, SSTI vs XSS vs OS command injection, benign detection, engine identification methodology, template context, realistic impact, Burp checks, and prevention. Does not publish exploit chains for every engine or claim every SSTI is RCE.
Related guides DVWA learning map
Short on time?Beta Get the gist of this 7-minute read in seconds, then ask follow-up questions

A server-side template injection vulnerability (SSTI) happens when untrusted input becomes template code, not template data.

In short:

  • Safe apps pass your value into a fixed template as data
  • Vulnerable apps build the template source from your input
  • The server-side engine then parses your directives

That is different from browser XSS and from OS command injection.

This lesson uses a tiny intentional Flask/Jinja2 lab on localhost. DVWA has no SSTI module, so this mini-app keeps proofs honest without a payload dump.

IMPORTANT
Run these steps only on systems you own or are authorized to test. Keep the lab on 127.0.0.1. Prefer arithmetic proofs such as {{7*7}}. Do not paste destructive payloads into production.

What SSTI is

Safe path:

text
template fixed → engine fills {{ name }} from a variable → HTML out

Vulnerable path:

text
user input → concatenated into template source → engine interprets attacker directives

Safe template data versus untrusted input concatenated into template source

On our lab app the bad line is:

python
return render_template_string("Hello " + name)
Output

Whatever arrives in name is compiled as Jinja2 source. That is the SSTI.


SSTI vs XSS vs command injection

Class Who interprets the input Typical proof Primary fix direction
XSS Browser / client script engine Markup or event handler runs in the page Context-aware output encoding / safe DOM APIs; CSP as defense-in-depth
SSTI Server-side template engine Arithmetic or directives evaluate in the response Do not build templates from user input
OS command injection Shell / OS command processor Extra OS command runs Do not shell out on concatenated strings

Quick checks:

  • Browser runs script → think XSS (XSS attack tutorial)
  • Server turns 7*7 into 49 in the HTML → think SSTI
  • OS runs an extra shell command → think command injection

Start the SSTI lab

Create a lab directory (authorized local use only):

bash
mkdir -p ~/ssti-lab

mkdir prints nothing when the folder is created or already exists. Save ~/ssti-lab/app.py with this route:

python
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.get("/")
def index():
    name = request.args.get("name", "guest")
    # Intentionally vulnerable: user input becomes template source
    return render_template_string("Hello " + name)

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5055, debug=False)
Output

Confirm Flask is importable:

bash
python3 -c 'from importlib.metadata import version; print(version("flask"))'
output
3.1.3

A version line means Flask is installed and ready. If import fails, install Flask with your distro package or a venv, then start the app in its own Terminal:

bash
python3 ~/ssti-lab/app.py

Leave that process running. You should see Flask listening on http://127.0.0.1:5055. Open a second Terminal for probes.

Shell tip: prefer --data-urlencode for probes with {{…}} so the shell does not expand braces.


Detect template evaluation

A safe SSTI detection payload should prove template evaluation without reading files or running OS commands. Send a normal name first:

bash
curl -s 'http://127.0.0.1:5055/?name=Alice'
output
Hello Alice

Plain text came back unchanged. That is the baseline.

Now try a benign Jinja2 expression:

bash
curl -s -G --data-urlencode 'name={{7*7}}' 'http://127.0.0.1:5055/'
output
Hello 49

What this shows:

  • The server evaluated {{7*7}}
  • 49 in the body is the SSTI signal
  • You did not need a browser alert dialog

In the browser, open the same URL and confirm Hello 49.

Engines use different syntax (Jinja2, Twig, Freemarker, Thymeleaf, and others). Start with a small arithmetic probe, then adjust from errors. Do not spray every public payload list at a live client.


Identify the template engine

Work methodically:

  1. Try a few syntax families ({{…}}, ${…}, #{…}, and similar)
  2. Read lab error messages when they appear
  3. Treat framework clues as a guess, then confirm with responses

Framework clues (not proof):

  • Flask often uses Jinja2
  • Symfony often uses Twig
  • Spring apps may use Thymeleaf

Do not identify an engine from {{7*7}} alone. Several engines can evaluate that. Fingerprint with multiple expressions and errors.

On this Jinja2 lab, ${7*7} should stay literal:

bash
curl -s 'http://127.0.0.1:5055/?name=${7*7}'
output
Hello ${7*7}

Nothing was evaluated. That syntax is not Jinja expression syntax here.

Next, try string multiplication. On Jinja2, {{7*'7'}} often becomes seven sevens. Twig can answer differently to the same expression:

bash
curl -s -G --data-urlencode "name={{7*'7'}}" 'http://127.0.0.1:5055/'
output
Hello 7777777

What this shows:

  • Jinja2-style evaluation on this lab
  • One probe is not enough to name the engine forever
  • Record several probe results together before you escalate

Understand template context

Where your input lands changes which probe works.

Plaintext / template-body context (this lab):

  • Template looks like Hello USER_INPUT
  • Lab code: "Hello " + name
  • {{7*7}} becomes new template syntax → 49 is a strong signal

Code / expression context:

  • Template looks like Hello {{ USER_INPUT }}
  • You are already inside an expression
  • Another {{7*7}} may break the template or do nothing useful
  • You often need to close or escape the current expression first

If a naked {{7*7}} fails, ask whether you are already inside an expression, attribute, or sandbox, not only whether "SSTI is absent."


Impact

Impact depends on engine, sandbox, and context:

  • Data exposure from objects the template can already reach
  • Object graph access in some configurations
  • File or OS access in some engines and configs
  • Server-side code execution when the engine exposes dangerous call paths

Severity can range from limited template tricks or data disclosure to server compromise. Confirmed evaluation is already a finding. Do not treat every SSTI as RCE. Escalate only when authorized and when the engine allows it.


Test with Burp Repeater

Proxy the lab with Burp Suite, open http://127.0.0.1:5055/?name=test, send the request to Repeater, and change name through:

  1. A unique marker (ssti-marker-1)
  2. {{7*7}}
  3. An alternate syntax family that should stay literal on Jinja2

Compare response bodies. Confirmed evaluation shows the computed value without a browser script dialog.


Prevention

Primary fix: do not construct templates from user input.

  • Keep template source in files or trusted code
  • Pass user values as data into a fixed template
  • Sandboxing limits what templates can reach; it is not a substitute for trusted template source
  • Autoescaping helps against some XSS in generated HTML; it does not stop SSTI when input is compiled as template syntax

Use a sandbox only when user-authored templates are a real product need, and keep that path narrow.


Summary

SSTI is untrusted input compiled as template code on the server. On the Flask/Jinja2 lab, Alice stays text, {{7*7}} becomes 49, and {{7*'7'}} helps fingerprint Jinja2 when compared with other probes. That is not XSS and not OS command injection.

Impact ranges from confirmed evaluation to deeper object or OS access. Prevention is fixed templates plus data parameters, not brace deny-lists or autoescape alone. For shell-backed features, use the OS command injection lesson instead.


References


Frequently Asked Questions

What does {{7*7}} mean when testing SSTI?

It is a benign detection payload. If the response contains 49 where the expression was, the server-side template engine likely evaluated the input as template code. Confirm the engine with additional probes before you assume Jinja2 or Twig.

Is SSTI the same as XSS?

No. XSS runs in the browser. SSTI is evaluated by the template engine on the server while building the response.

Does every SSTI mean remote code execution?

No. Confirmed template evaluation is already a finding. Whether you reach OS commands depends on the engine, sandbox, and configuration.

Why use a local Flask app instead of DVWA?

DVWA focuses on other classic modules. A tiny intentional Jinja2 app keeps SSTI proofs reproducible without depending on a DVWA feature that is not there.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)