7 Powerful Server-Side Template Injection Defenses
Server-Side Template Injection (SSTI) Detection, Exploitation & Defense in Modern Apps
Server-side template injection (SSTI) is one of those bugs that hides in “normal” features: email templates, invoice PDFs, CMS themes, notification builders, localization strings, even “advanced search” UIs that support placeholders. It’s elusive because the vulnerability often lives one abstraction away—a dynamic template stack, indirect render calls, or content that gets stored first and rendered later.
This guide focuses on SSTI detection, realistic template engine security patterns (Jinja2, Twig, Velocity), safe proof methods, and production-ready defenses—plus logging and evidence capture you’ll want if SSTI becomes an incident.

If you want a full assessment beyond quick scanning, start here: Risk Assessment Services and our fix support: Remediation Services.
What is SSTI (and why it stays hidden)
Server-side template injection happens when an application renders a template using untrusted input as template code, not just as data. Unlike XSS (browser), SSTI executes on the server inside a template engine runtime. Depending on engine configuration and exposed objects, impact can range from:
- sensitive data exposure (configuration, tokens, secrets)
- authorization bypass or business logic manipulation
- SSRF-like behaviors via helper functions (varies by app)
- in worst cases, code execution or sandbox escape (engine + environment dependent)
Why it stays hidden:
- Templates can be indirectly called (helper renders partials, layout inheritance, background jobs)
- Input can be second-order (stored then rendered later)
- Rendering can happen in non-HTTP paths (PDF generation, email delivery workers)
- The same UI field might be “safe text” in one flow and “template” in another
Common template engines and “injection shapes”
Different engines have different syntax, but SSTI testing usually starts with safe arithmetic probes and render error fingerprints.
Quick, safe probe patterns (non-destructive)
Use these to confirm evaluation without attempting command execution:
| Engine family | Typical expression | Safe probe |
|---|---|---|
| Jinja2 / Nunjucks-like | {{ ... }} | {{7*7}} → expect 49 |
| Twig | {{ ... }} | {{7*7}} → expect 49 (if expression allowed) |
| Velocity | #set() / $var | #set($x=7*7)$x → expect 49 |
| Freemarker | ${ ... } | ${7*7} → expect 49 |
If the response doesn’t show
49, don’t stop—SSTI is often blind (rendered in email/PDF), encoded, or only visible in downstream artifacts.
Recon tactics that actually find SSTI in modern stacks
SSTI discovery is mostly about finding where template compilation happens and whether user input can influence it.
1) Parameter inheritance & “template context drift”
Look for endpoints where one parameter becomes another layer’s input:
subject,body,signature,header,footerwelcome_message,email_template,sms_templatepdf_title,invoice_note,report_footertheme,layout,widget,partial
Tell-tale sign: the value is stored, reused, or rendered in a different place than where it was submitted.
2) Reflected templates vs indirect template calls
- Reflected SSTI: immediate response renders user-controlled content.
- Indirect SSTI: content flows into a template helper (e.g.,
render(body)inside a layout). - Second-order SSTI: payload stored in DB, later rendered in an admin view, PDF, email, or webhook retry flow.
3) Find “template-like” features hiding in plain sight
High-probability areas:
- marketing emails / campaigns
- multi-tenant branding (“custom footer”)
- document generation (invoice, export, statements)
- notification rule builders (“use placeholders like {{name}}”)
- translation/localization strings
4) Observe render boundaries
During testing, capture:
- where the value appears (HTML, PDF, email, logs)
- whether it’s escaped/encoded
- whether errors reveal engine names (Jinja2/Twig/Velocity)
Exploitation: validating impact safely (RCE vs sandbox escape)
A common mistake is jumping straight from “it evaluates” to “try RCE payloads.” In real engagements, the safer and more credible approach is to prove exploitability with non-destructive impact markers.
A safer impact ladder (recommended)
- Evaluation confirmed: arithmetic probe renders (
49) or engine-specific error appears - Context exposure: can you read permitted variables (e.g.,
user.name,tenant) - Sensitive data exposure (without harming systems): prove access to non-secret environment metadata or app constants (varies by app)
- Privilege boundary test: does one user influence another user’s output (stored template → multi-user exposure)
- High-risk path analysis: identify whether dangerous helpers/objects exist that could lead to file access, network calls, or code execution
- If required: escalate only under explicit authorization, controlled conditions, and documented approvals
RCE vs sandbox escape (what changes risk)
- RCE risk increases when the engine exposes powerful objects/functions (helpers that can call OS, load classes, access filesystem, etc.).
- Sandbox escape becomes relevant when sandboxing is enabled but misconfigured, or when the runtime still exposes dangerous primitives indirectly.
Defender takeaway: assume attackers will look for object exposure, not “magic payloads.” The best defense is minimizing what templates can access.
If you suspect active exploitation already, involve DFIR early: Digital Forensic Analysis Services.
Real-world vulnerable patterns (and secure fixes) — with code
Below are common “this looked convenient” patterns that create SSTI.
Jinja2 (Python/Flask) — vulnerable vs fixed
❌ Vulnerable: rendering untrusted template strings
from flask import Flask, request
from flask import render_template_string
app = Flask(__name__)
@app.post("/preview")
def preview():
user_template = request.form["template"] # attacker-controlled
name = request.form.get("name", "Guest")
return render_template_string(user_template, name=name)✅ Fixed: render trusted templates, pass data only
from flask import Flask, request, render_template
app = Flask(__name__)
@app.post("/preview")
def preview():
name = request.form.get("name", "Guest")
# Only render templates shipped with the app (trusted)
return render_template("preview.html", name=name)✅ Fixed: if you truly need “templates,” use strict allowlists
Pattern: allow placeholders, not code. Example: replace {name} tokens only.
import re
ALLOWED_TOKENS = {
"name": lambda ctx: ctx["name"],
"company": lambda ctx: ctx["company"],
}
TOKEN_RE = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
def safe_token_render(text: str, ctx: dict) -> str:
def repl(m):
key = m.group(1)
if key not in ALLOWED_TOKENS:
return m.group(0) # leave unknown tokens untouched
return str(ALLOWED_TOKENS[key](ctx))
return TOKEN_RE.sub(repl, text)
# Usage:
# output = safe_token_render("Hello {name}", {"name":"Asha","company":"Acme"})Why it works: you avoid exposing a full template language runtime to untrusted input.
Jinja2 hardening: strict contexts + safer environment defaults
If you must use a Jinja2 environment for controlled templates:
- enforce
StrictUndefined - keep a small allowlist of filters/tests
- avoid passing rich objects (request, config, db handles)
from jinja2 import Environment, StrictUndefined
env = Environment(
autoescape=True,
undefined=StrictUndefined,
)
# Optional: remove/avoid dangerous filters, add only safe ones you need
env.filters = {
"upper": str.upper,
"lower": str.lower,
"title": str.title,
}
template = env.from_string("Hello {{ name|title }}")
print(template.render({"name": "shofiur"}))Twig (PHP) — vulnerable vs fixed
❌ Vulnerable: compiling user input as a template
$twig = new \Twig\Environment($loader);
$userTemplate = $_POST['template']; // attacker-controlled
echo $twig->createTemplate($userTemplate)->render(['name' => 'Guest']);✅ Fixed: load only server-side templates
// $loader points to trusted template directory
$twig = new \Twig\Environment($loader, ['autoescape' => 'html']);
echo $twig->render('preview.twig', ['name' => 'Guest']);✅ Fixed: sandbox (when you must allow limited templating)
Use a strict allowlist of tags/filters/functions. Keep it minimal and business-driven.
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\Extension\SandboxExtension;
use Twig\Sandbox\SecurityPolicy;
$loader = new ArrayLoader([]);
$twig = new Environment($loader, ['autoescape' => 'html']);
$allowedTags = ['if', 'for'];
$allowedFilters = ['escape', 'upper', 'lower'];
$allowedMethods = [];
$allowedProperties = [];
$allowedFunctions = []; // keep empty unless truly needed
$policy = new SecurityPolicy(
$allowedTags, $allowedFilters, $allowedMethods, $allowedProperties, $allowedFunctions
);
$twig->addExtension(new SandboxExtension($policy, true));Velocity (Java) — vulnerable vs fixed
❌ Vulnerable: evaluating untrusted template strings
VelocityContext ctx = new VelocityContext();
ctx.put("name", request.getParameter("name"));
String userTpl = request.getParameter("template"); // attacker-controlled
StringWriter out = new StringWriter();
Velocity.evaluate(ctx, out, "logTag", userTpl);
return out.toString();✅ Fixed: use trusted templates (files/resources), data only
Template tpl = velocityEngine.getTemplate("preview.vm", "UTF-8");
VelocityContext ctx = new VelocityContext();
ctx.put("name", safeName);
StringWriter out = new StringWriter();
tpl.merge(ctx, out);
return out.toString();✅ Hardening idea: restrict what the context contains
Most Velocity SSTI impact comes from powerful objects placed into context. Keep context primitives simple:
- strings, numbers, booleans
- pre-rendered safe snippets
- never raw request/session objects
- never service clients, filesystem, or class loaders
Mitigations that hold up in production
These are the controls that consistently prevent SSTI exploitation across stacks.
1) Separate templates from user input (best)
- Templates live in source control
- Users only provide data
- Use approved placeholders, not a full template language
2) Strict contexts (small, explicit allowlists)
- Pass only required fields
- Avoid passing “god objects” (request, app config, DB, session, framework globals)
3) Output encoding + autoescape (necessary, not sufficient)
Autoescape helps against HTML injection, but does not inherently stop SSTI if user input becomes template code.
4) Safe sub-expressions / placeholder mode
If the business requires “mail-merge” behavior, implement a token renderer (like {first_name}) instead of compiling templates.
5) Sandbox correctly (and assume bypass attempts)
Sandboxing can reduce risk, but:
- it must be configured with tight allowlists
- it must be tested regularly (pentest + regression tests)
6) CI/CD guardrails (stop regressions)
Add checks in code review + pipelines:
- detect
render_template_string,createTemplate(userInput),Velocity.evaluate(userInput) - require security review for “template editor” features
Example Semgrep-style pattern ideas (conceptual):
rules:
- id: python-flask-ssti-render-template-string
patterns:
- pattern: render_template_string(...)
message: "Potential SSTI: render_template_string used. Ensure template is not attacker-controlled."
severity: WARNING
languages: [python]Detection tools and automated scanning heuristics (safe automation)
Automated SSTI detection works best when it combines:
- multiple probe syntaxes
- response comparison (baseline vs injected)
- error fingerprinting
- second-order checks (store then render)
A practical DAST-style probe list (safe)
Use a small set of non-destructive payloads:
{{7*7}}${7*7}#set($x=7*7)$x{{ "a" ~ "b" }}(engine-dependent concatenation)- malformed braces to trigger parser errors (use sparingly)
Minimal scanner example (Python) — safe probes only
import requests
from urllib.parse import urlencode
PROBES = [
("jinja_like", "{{7*7}}", "49"),
("freemarker_like", "${7*7}", "49"),
("velocity_like", "#set($x=7*7)$x", "49"),
]
def test_ssti(url: str, param: str = "q", timeout: int = 10):
baseline = requests.get(url, timeout=timeout).text
findings = []
for engine, payload, expect in PROBES:
qs = urlencode({param: payload})
r = requests.get(f"{url}?{qs}", timeout=timeout)
body = r.text
# Heuristic A: expected evaluation appears and is new vs baseline
if expect in body and expect not in baseline:
findings.append((engine, "evaluated_probe", payload))
# Heuristic B: template/parser error hints (lightweight)
error_hints = ["TemplateSyntaxError", "Twig\\Error", "VelocityException"]
if any(h in body for h in error_hints):
findings.append((engine, "error_fingerprint", payload))
return findings
if __name__ == "__main__":
for f in test_ssti("https://target.example/search", param="query"):
print("Possible SSTI:", f)Second-order SSTI test approach
If your app stores templates (tickets, profiles, CMS blocks):
- submit payload in a field that is later rendered elsewhere
- retrieve the downstream artifact (admin view, PDF export, email preview)
- look for
49or template errors in that downstream output
This is where many “we scanned and found nothing” programs miss SSTI.
Logging & evidence capture for incident response
If SSTI is exploited, you’ll want to answer:
- which template path was used (feature + endpoint)
- what input became template code (original + rendered)
- who triggered it (user ID, API key, tenant)
- what was accessed/changed downstream
- when it started and whether it persisted (stored templates)
What to log (high value, low noise)
- request ID / trace ID
- authenticated principal + tenant
- template name / render function name
- whether source was trusted (file) or untrusted (string/db)
- render errors (exception type + message)
- hash of the template content (store content securely, redact secrets)
- destination: HTML response, email job, PDF job
Example structured event (JSON):
{
"event": "template_render",
"ts": "2026-02-24T12:40:11Z",
"request_id": "req_8f21c1",
"user_id": "u_12931",
"tenant": "t_acme",
"engine": "jinja2",
"template_source": "db",
"template_id": "email_welcome_v3",
"template_hash": "sha256:...redacted...",
"result": "error",
"error_type": "TemplateSyntaxError"
}If you need investigation and containment support: Digital Forensic Analysis Services.
Free Tool + Sample Report
Free Website Vulnerability Scanner dashboard (from our tool page)

Sample report to check Website Vulnerability (by the tool)

Quick note: automated tools are a great start, but SSTI often requires manual verification and second-order testing. For end-to-end validation, see Risk Assessment Services.
Practical next steps (a simple operational plan)
- Inventory where templating exists (emails, PDFs, CMS, notifications)
- Decide: placeholders-only vs sandboxed templates
- Lock context: allowlist variables, remove rich objects
- Add CI/CD checks to prevent dangerous render APIs
- DAST safely with arithmetic probes + second-order tests
- Log template renders with IDs/hashes for DFIR readiness
- Fix & verify with retesting and regression coverage
If you want help executing this safely in production: Remediation Services.
Related recent reads from our blog
Here are a few recent Pentest Testing Corp posts that pair well with this SSTI defense playbook:
- 9 Proven API Abuse Detection Plays WAFs Miss
- 7 Powerful Risk-Driven API Throttling Tactics
- 9 Powerful Webhook Security Patterns That Stop Breaches
- 7 Powerful Forensic Readiness Steps for SMBs
Dedicated Service Pages
- Validate risk across apps/APIs: Risk Assessment Services
- Fix findings with expert support: Remediation Services
- If compromise is suspected: Digital Forensic Analysis Services
🔐 Frequently Asked Questions (FAQs)
Find answers to commonly asked questions about Server-Side Template Injection (SSTI).

