FlawOpen

flawopen.com/Vulnerabilities/Server-Side Template Injection (SSTI)

Server-Side Template Injection (SSTI):原因と修正

Impact depends on reachable template behavior CWE-1336 Template Engine Security
💡 やさしい説明 — データかプリンターへの命令か

「こんにちは、{{ name }}」という定型文で手紙を印刷する会社を想像してください。安全な方法では、固定された定型文と顧客名をデータとしてプリンターに渡します。危険な方法では、印刷前に顧客の文字列を定型文の指示に挿入します。その文字列をテンプレートエンジンが構文として解釈すると、Server-Side Template Injection(SSTI)になります。影響はエンジン、設定、公開されたオブジェクト、プロセス権限によって異なり、式の評価だけでOSコマンドが実行されるわけではありません。

対象: Jinja、Twig、FreeMarker、ERBなどのテンプレートソース構築とサーバー側レンダリング。
脆弱な境界: 信頼できない入力を信頼済みテンプレートの値として渡さず、テンプレートソースとしてコンパイルまたは解析すること。
影響: テンプレート式の評価。追加の影響はエンジン機能、公開オブジェクト、サンドボックス、プロセス権限に依存します。
対策: テンプレートソースを信頼できる固定内容に保ち、入力はコンテキストデータとしてのみ渡します。ユーザー作成テンプレートが必要ならレンダリングを分離します。

主要概念とサブシステム用語

テンプレートエンジン
JinjaやTwigなど、テンプレート構文を解析し、テンプレートとコンテキストデータから出力を生成するサーバー側コンポーネント。
テンプレートソースへのインジェクション
外部テキストが解析前にソースへ組み込まれ、データではなくテンプレート構文として解釈される不備。
コンテキストデータ
信頼できる固定テンプレートに別途渡す値です。適切に分離すると、{{ 7 * 7 }}のような値をソースとして再解析しません。
CWE-1336
テンプレートエンジンで使われる特殊要素の不適切な無害化。外部入力をテンプレート構文として危険に処理する弱点です。

攻撃の流れを段階ごとに解説

Step 1

攻撃者の入力がテンプレート処理へ届く

フォーム入力、保存済みプロフィール値など、外部から影響される文字列がテンプレートを作成・コンパイルするコードへ届きます。問題は値の通常表示ではなく、ソースの構築です。

Step 2

アプリがデータとテンプレート指示を混ぜる

静的テンプレートと別のコンテキスト値を使わず、信頼できない文字列をテンプレートソースへ埋め込みます(例:f-stringをrender_template_stringに渡す)。

Step 3

エンジンが結合されたソースを解析する

エンジンは言語と設定に従って式を解釈します。無害な算術式は評価の確認に使えますが、それだけでファイル、秘密情報、システムコマンドへのアクセスは証明されません。

Step 4

影響はエンジンと実行環境に依存する

追加の影響はアクセス可能なオブジェクトや関数、サンドボックスの動作、エンジン制限、Webプロセスの権限に左右されます。一部の設定でRCEは可能ですが、すべてのSSTIで自動的に起きるわけではありません。

仕組みと根本原因

SSTIは、外部から影響されるテキストを解析前にテンプレートプログラムの一部にしてしまうことで発生します。Flask/Jinjaでは、リクエスト値から新しいソース文字列を作り、render_template_string()に渡すのが危険なパターンです。HTMLエスケープは出力を保護するもので、信頼できないテンプレートソースをデータに変えるものではありません。固定テンプレートを読み込み、値をコンテキストとして別に渡すのが修正です。ユーザーにテンプレートを作らせる場合は、最小限のデータだけを公開し、リソース制限のある分離workerでレンダリングしてください。サンドボックスは多層防御であり、秘密や強力なアプリケーションオブジェクトを公開する理由にはなりません。

flask_app.py(脆弱なパターン)脆弱
# VULNERABLE: request data becomes template source before parsing.
from flask import Flask, request, render_template_string
app = Flask(__name__)

@app.route("/greet")
def greet():
    name = request.args.get("name", "Guest")
    # A value such as {{ 7 * 7 }} is interpreted by the template engine.
    source = f"<h1>Welcome, {name}!</h1>"
    return render_template_string(source)
flask_app.py(安全な修正)安全
# HARDENED: fixed template source; request data stays a context value.
from flask import Flask, request, render_template
app = Flask(__name__)

@app.route("/greet")
def greet():
    name = request.args.get("name", "Guest")
    return render_template("greet.html", name=name)

# templates/greet.html is trusted, static source:
# <h1>Welcome, {{ name }}!</h1>

攻撃シーケンスと概念例

エンジニア向け防御・予防ルール

一次資料と関連するセキュリティトピック:セキュリティディレクトリを見る →
CWE-1336:テンプレートエンジンへのインジェクション → OWASP WSTG:Server-Side Template Injection → Jinja:サンドボックスとセキュリティ → Jinja API:テンプレートとloader → Flask:テンプレートのレンダリング →