テンプレートインジェクション(SSTI)の深淵:なぜ「ただの文字列」がサーバーを乗っ取るのか
こんにちは。現場で日々、防御側の視点からシステムを磨き上げているエンジニア諸君。
今日は「SSTI(Server-Side Template Injection)」について話そう。多くのエンジニアが「テンプレートエンジンを使っているから安全だ」と信じ込んでいる。だが、その信頼こそが最も危険な脆弱性の入り口になる。
SSTIは、単なるWebアプリのバグではない。「データとして扱うべき入力を、エンジンが実行コードとして解釈してしまう」という設計上の致命的な勘違いだ。一度ここを突破されると、OSコマンドの実行(RCE)から環境変数のダンプ、内部ネットワークへの侵入まで一直線だ。教科書的な「入力をサニタイズしろ」といった抽象的なアドバイスは忘れて、現場で通用する「勝ち筋」の話をしよう。
—
1. なぜ「Jinja2」はハッカーに愛されるのか
PythonのJinja2やFlask環境において、開発者がよくやるミスがこれだ。
【危険なコード例】絶対に真似してはならない
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route(“/greet”)
def greet():
name = request.args.get(“name”, “Guest”)
# ユーザー入力を直接テンプレート文字列に埋め込んでいる!
template = f”
Hello, {name}!
”
return render_template_string(template)
このコードに対し、攻撃者はこのようなリクエストを送る。
/greet?name={{77}}
もしページに Hello, 49! と表示されたら、そこはSSTIの温床だ。これはJinja2が {{ ... }} の中身を式として評価している証拠であり、攻撃者はこの「計算機」を使って、Pythonのクラス構造を辿り、最終的に os.popen('id').read() を実行するペイロードを送り込んでくる。
—
2. SSTIを確実に防ぐための「鉄則」
現場でSSTIを完全に無力化するには、以下の2つの防衛ラインを必ず守れ。
防衛ライン①:入力値とテンプレートを物理的に分離する
最も確実な対策は、テンプレートエンジンの「式評価機能」にユーザー入力を渡さないことだ。テンプレートはあくまで「表示用」であり、ユーザー入力は「変数」として渡すべきだ。
【セキュアな実装例:Python/Flask】
@app.route(“/greet”)
def greet():
name = request.args.get(“name”, “Guest”)
# テンプレート文字列に埋め込むのではなく、引数として渡す!
# これにより、nameは単なる「文字列」として扱われ、評価は一切行われない。
return render_template_string(“
Hello, {{ name }}!
“, name=name)
防衛ライン②:サンドボックスの限界を理解する
多くのテンプレートエンジンにはサンドボックス(安全な実行環境)があるが、言語の仕様を完全に制限しきるのは不可能に近い。特にPythonのような動的言語では、__subclasses__() を呼び出すだけで、いとも簡単にサンドボックスから脱出できる。
だからこそ、「サンドボックスの設定で守る」という発想を捨て、「ユーザー入力を評価させない」という設計に徹するんだ。
—
3. インフラレイヤーでの多層防御(WAF/Nginx)
アプリケーション側の修正がすぐにできない場合、あるいはゼロトラストの観点から念には念を入れたい場合は、WAFでガードする。
【AWS WAFのルール設定例(イメージ)】
SSTIのペイロードには、{{ や {% といったテンプレート特有の記号が必ず含まれる。これらを検知対象にする。
- Match criteria:
{{または{%__class__(Pythonオブジェクトの構造を辿る際に必須)os.popenやimportといった危険な関数名- Action: Block
また、Nginxで特定の特殊文字を含むリクエストを弾くのも有効だ。
nginx.conf の locationブロックに追記
URL内にテンプレートの特殊記号が含まれる場合、即座に403を返す
if ($query_string ~ “(\{\{|\{\%|\}\}|\%\})”) {
return 403;
}
—
4. チーフからの教訓:最後に
SSTIのような攻撃は、開発者の「ちょっとした便利機能の実装」から始まる。コードを書くとき、常に自分に問いかけてほしい。
> 「この変数は、テンプレートエンジンが解釈すべき『命令』なのか? それとも単なる『表示用データ』なのか?」
もし後者であれば、render_template_string のような動的なテンプレート生成を避け、静的なテンプレートファイルを利用する設計に切り替えるべきだ。
セキュリティとは、穴を塞ぐことではない。「穴が開かない構造」を設計することだ。現場での実装において、この原則を忘れないようにしてくれ。
もし実装中に「これ、本当に安全か?」と迷うような複雑なケースがあれば、いつでも相談に来い。君たちの書くコードが、誰かの脅威にならないことを祈っている。
コメント