Server-side template injection (SSTI): how it works, how testers detect it, how to fix it

Server-side template injection (SSTI) is a flaw where user input becomes part of a template that the server then evaluates, so the attacker's expression runs on the server. Fix it by passing user input to templates as data, never as template source, and by not letting users edit templates. Testers prove it with a harmless arithmetic probe and stop there.

Server-side template injection (SSTI) is a flaw where user input becomes part of a template's source code, so the server's template engine evaluates the attacker's expression. MITRE describes CWE-1336 as a product that uses a template engine to insert or process externally-influenced input and does not neutralize syntax that the engine could run as template logic or code. Depending on the engine, the result ranges from reading data to running code on the server.

Testing for SSTI is part of web application penetration testing and API penetration testing. Testing needs written authorization from the system owner.

What is server-side template injection?#

SSTI is injection into the template, not into the data the template displays. Template engines such as Jinja2, Twig and Freemarker mix fixed text with placeholders. A safe application passes user input to a fixed template as a variable. A vulnerable application puts the input inside the template text before the engine reads it.

OWASP's Top 10:2025 groups template injection under A05 Injection. PortSwigger's academy explains the usual causes: developers let users edit templates for emails or page layouts, or they build a template string by concatenation, which is the simplest mistake.

How does SSTI work?#

SSTI works when a user value is concatenated into a template string and the string is rendered. The engine cannot tell which part was written by the developer and which part came from the user.

Vulnerable greeting page (fictional app)

This is a demonstration. Greetly is a fictional card app on app.example.com, and no real customer system was tested.

The app shows a thank-you message. The handler builds the template text from the name.

# Vulnerable: user input becomes template source
@app.get("/thanks")
def thanks():
    name = request.args.get("name", "friend")
    return render_template_string("<p>Thanks, " + name + "!</p>")

Normal request: GET /thanks?name=Ada returns Thanks, Ada!.

The tester sends a harmless arithmetic expression in Jinja2 syntax:

GET /thanks?name={{7*7}} HTTP/1.1
Host: app.example.com

The response says Thanks, 49!. The server computed the value, so the input reached the template engine. The control name={{7*7 (an unclosed expression) should give an error or a literal result, and name=7*7 should return Thanks, 7*7!. The tester records the three responses and stops.

The probe {{7*7}} is the same one PortSwigger and OWASP's testing guide use. It proves evaluation without touching the system.

Why is SSTI not the same as XSS?#

SSTI and XSS can look alike in a test because both reflect input in a page. The fork is the arithmetic probe: if the response shows 49, the server evaluated the input. PortSwigger notes that this plaintext case is "often mistaken for a simple XSS vulnerability".

Observation What it suggests Next control
{{77}} returns {{77}} unchanged The server did not evaluate it Check for HTML context and XSS separately
{{7*7}} returns 49 A template engine evaluated it (SSTI) Identify the engine with the probe table
<b>x</b> renders as bold, {{7*7}} unchanged Reflected HTML without template evaluation (XSS class) See cross-site scripting
A template error message appears The input reached a template parser Read the error for the engine name
Response changes only in the browser after scripts run Client-side evaluation, for example a front-end framework Test in a real browser and treat as a different class

The impact differs. XSS attacks a user's browser session. SSTI attacks the server, and in many engines it leads to code execution on the host. Both can exist in the same field.

How do testers detect SSTI and identify the engine?#

Testers detect SSTI with a set of harmless probes, then narrow down the engine from the results. PortSwigger describes a two-step method. First fuzz with characters common in template syntax, such as ${{<%[%'"}}%</code>. An error suggests the input is being read as template code. Then submit math expressions in the syntax of the engines that may be in use.

Contexts matter. In a plaintext context the input appears in the page body, and {{7*7}} or ${7*7} can work as is. In a code context the input lands inside an existing expression. Testers then close the expression and check that appended HTML renders next to normal output, which PortSwigger calls "a key indication" of SSTI.

One result does not identify an engine. PortSwigger's example is {{7*'7'}}: it gives 49 in Twig and 7777777 in Jinja2.

This table lists arithmetic probes for 10 engines, based on each engine's own expression syntax. Run one probe, read the result, and stop.

Engine Language Arithmetic probe What a vulnerable result shows
Jinja2 Python {{7*7}} 49. The string probe {{7*'7'}} gives 7777777
Twig PHP {{7*7}} 49. The string probe {{7*'7'}} gives 49
Freemarker Java ${7*7} 49
Velocity Java #set($x=7*7)$x 49
Thymeleaf Java [[${7*7}]] 49 (inline expression)
Handlebars JavaScript {{7*7}} The text stays literal, because the language has no arithmetic. A literal result does not prove safety of other fields
Pug JavaScript #{7*7} 49
ERB Ruby <%= 7*7 %> 49. A malformed <%=foobar%> gives an error that names ERB
Smarty PHP {7*7} 49
Razor .NET @(7*7) 49

Handlebars and Mustache are logic-less engines. PortSwigger recommends them where possible, because keeping logic apart from presentation limits the worst attacks.

Which real incidents involved template injection?#

A documented case is CVE-2023-22527 in Atlassian Confluence Data Center and Server. NVD published it on 16 January 2024 and describes "a template injection vulnerability on older versions" that allows an unauthenticated attacker to achieve RCE on an affected instance. NVD maps it to CWE-74, gives a CVSS v3.1 score of 9.8, and lists a CISA Known Exploited Vulnerabilities entry dated 24 January 2024. The record says most recent supported versions are not affected, and Atlassian recommends installing the latest version.

The case shows a pattern: a request parameter that reached a template engine on an internet-facing server, with no login needed. This page does not describe the exploit. Read the NVD record and the vendor advisory for affected versions and fixes.

Do sandboxes stop SSTI?#

A sandbox lowers the risk but does not remove it. The Jinja documentation says "the sandbox alone is not a solution for perfect security". It also warns that templates can still raise errors, and that "a relatively small template" can produce "a very large amount of output", so CPU and memory limits are advised. PortSwigger says sandboxing untrusted code is inherently difficult and prone to bypasses, and that exploitation is especially easy in unsandboxed environments.

Treat a sandbox as one layer. The primary control is to keep user input out of template source. The second layer is isolation: PortSwigger suggests running the template environment in a locked-down Docker container, on the assumption that code execution may eventually happen. Pass only data that the template needs, and avoid global objects with side effects (Jinja's advice).

How do you prevent SSTI?#

Prevent SSTI by treating user input as data. Pass it as a variable to a fixed template, and never build template text from it. If users must edit templates, use a logic-less engine, restrict what the template can reach, and render in an isolated process.

Flask and Jinja2 (Python)

# Vulnerable
return render_template_string("<p>Thanks, " + name + "!</p>")

# Safe: fixed template, user value passed as data (autoescape on)
return render_template_string("<p>Thanks, {{ name }}!</p>", name=name)
# Better: a template file in the templates folder
return render_template("thanks.html", name=name)

Django templates (Python)

from django.shortcuts import render
return render(request, "thanks.html", {"name": name})

Do not build a Template(...) object from user text. If users edit templates, use a sandboxed environment such as Jinja's ImmutableSandboxedEnvironment, with CPU and memory limits, as the Jinja documentation advises.

Express with Pug or EJS (Node.js)

// Vulnerable: user input compiled as a template
res.send(pug.render("p Thanks, " + name));

// Safe: fixed template file, user value as local data
res.render("thanks", { name });

In Pug, #{name} escapes output and !{name} does not. Keep untrusted values out of the template string and out of unescaped output.

Spring and Thymeleaf (Java)

// Safe pattern: fixed view name, data in the model
model.addAttribute("name", name);
return "thanks";

Do not build a view name or a fragment expression from request input. Use th:text for output, which escapes the value.

Rails and ERB (Ruby)

# Vulnerable
render inline: "<p>Thanks, #{params[:name]}!</p>"

# Safe: fixed template, value escaped on output
# app/views/cards/thanks.html.erb:  <p>Thanks, <%= params[:name] %>!</p>
render :thanks

Never pass user input to render inline: or ERB.new.

Twig, Smarty and Razor. Use the same pattern: load templates from files, pass user values as variables, and do not call the engine's string-render function with user text.

These examples show the pattern. Test them in your own framework version. Add a code search for string-render calls (render_template_string, Template(, render inline, ERB.new, pug.render, createTemplate).

What is the SSTI testing checklist?#

Use this list on each place where user input shows up in rendered output, and only inside the agreed scope.

  1. List the places that render user text: greetings, emails, PDF and invoice templates, error pages, CMS and marketing editors, notification text, report builders.
  2. Send the fuzz string for template characters and watch for errors.
  3. Send the arithmetic probe for the likely engine. Compare with controls: a literal string, an unclosed expression and a HTML tag.
  4. If you get 49, identify the engine with a second probe ({{7*'7'}}) and error messages.
  5. Test code contexts: close the expression, then add a harmless tag.
  6. Check stored values that are rendered later in emails, PDFs and admin pages, which may need a second session to read back.
  7. Stop at the arithmetic proof. Running commands or reading files needs separate written approval in the scope.
  8. Save requests, responses and timestamps. Report controls that held, such as parameters that escape output correctly.
  9. Review the code for string-render calls and user-editable template features.

SSTI often sits next to OS command injection, because code execution in a template engine can start system commands. Test both when a template feature exists.

When should you get a professional test?#

Get a professional test when your application renders user-influenced text in emails, documents or admin pages, or when you let customers edit templates. Scanners catch simple reflected probes. Stored and second-order cases, such as a value rendered in an invoice PDF a day later, need controls and a read-back.

Excession Labs runs authorized web application penetration tests and API penetration tests. We agree the scope with you in writing, check the evidence for each finding and write the report. You can also apply for the free pentest pilot.

Questions

What is the difference between SSTI and XSS?

XSS runs attacker script in the victim's browser. SSTI runs an attacker's expression in the server's template engine. An arithmetic probe such as 7*7 in double curly braces that returns 49 means the server evaluated it, which points to SSTI. If the input comes back unchanged and only HTML tags execute in the browser, that points to XSS. PortSwigger notes that SSTI is often mistaken for simple XSS.

How do you detect SSTI safely?

Send a harmless arithmetic expression in the template syntax of the suspected engine and look for the computed result, for example 49 for 7*7. Add controls such as a literal string and a malformed expression. Stop at the arithmetic proof. Running commands needs separate written approval in the scope.

Does sandboxing stop SSTI?

A sandbox reduces the risk but does not remove it. The Jinja documentation says the sandbox alone is not a solution for perfect security. PortSwigger says sandboxing untrusted code is inherently difficult and prone to bypasses. Keep user input out of template source, and isolate the rendering process as a second layer.

Is rendering user input in a template always SSTI?

No. Passing user input as a variable to a fixed template is safe, and the engine escapes or prints it as text. SSTI needs the input to become part of the template source, for example through string concatenation, a template editor or a render-from-string call.

Which template engines are affected by SSTI?

Any engine that evaluates expressions can be affected if input reaches the template source. Jinja2, Twig, Freemarker, Velocity, Thymeleaf, Pug, ERB, Smarty and Razor all evaluate expressions. Logic-less engines such as Mustache and Handlebars reduce the impact because their templates have little or no logic.

Sources

  1. PortSwigger Web Security Academy: Server-side template injection (checked 9 November 2026)
  2. OWASP WSTG: Testing for Server-side Template Injection (checked 9 November 2026)
  3. CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine (checked 9 November 2026)
  4. OWASP Top 10:2025 (checked 9 November 2026)
  5. Jinja documentation: Sandbox (checked 9 November 2026)
  6. NVD: CVE-2023-22527 (Atlassian Confluence) (checked 9 November 2026)