SSTIの深淵:テンプレートエンジンを「シェル」に変える悪魔の誘惑
現場でよくある光景だ。「ユーザー入力を柔軟にHTMLに埋め込みたい」という理由で、安易にテンプレートエンジンを導入する。開発者は「まあ、XSS対策はしてるから大丈夫だろう」と高を括る。だが、そこにはSQLインジェクションすら霞む、システムの心臓部を差し出す「SSTI(Server-Side Template Injection)」という落とし穴が口を開けている。
今日は、テンプレートエンジンがなぜ「リモートコード実行(RCE)」の踏み台にされるのか、そのメカニズムと、防御側の我々がどう立ち回るべきかを徹底的に解説する。
—
なぜテンプレートエンジンは「危険な場所」なのか
テンプレートエンジン(Jinja2, FreeMarker, Twig等)の本質は、プログラム内の変数をテンプレートに流し込み、動的に文字列を生成することだ。しかし、これらは往々にして「言語のメタデータ」や「内部オブジェクトの探索」を許容するように設計されている。
攻撃者は、この「探索機能」を悪用して、本来アクセスできるはずのないクラスや関数を呼び出す。例えばPythonの Jinja2 であれば、{{ ''.__class__.__mro__[1].__subclasses__() }} のような記述で、現在の実行環境にロードされている全てのモジュールを列挙できてしまう。ここで os モジュールや subprocess モジュールを見つけ出し、コマンドを実行するのがSSTIの基本戦略だ。
攻撃の一例:Jinja2でのRCE(概念実証)
もし、ユーザー入力をそのまま render_template_string() に渡すような実装があれば、攻撃者は以下のようなペイロードを投げてくるだろう。
# 攻撃者が投げるペイロードの例(base64エンコード等で隠蔽されることが多い)
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
この一行で、サーバーのOSコマンド id が実行され、レスポンスとして権限情報が返ってくる。これが意味するのは「Webアプリのプロセス権限で、サーバー上のあらゆる操作が可能になった」という終わりの始まりだ。
—
防御の最前線:開発者が守るべき「聖域」
SSTIを完全に防ぐための鉄則はシンプルだ。「ユーザー入力をテンプレートの一部として解釈させないこと」。これに尽きる。
Python (Flask/Jinja2) における安全な実装
よくある間違いは、文字列結合でテンプレートを作成することだ。以下のように、変数をテンプレートの「データ」として渡すのが正しい作法である。
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route("/greet")
def greet():
user_name = request.args.get("name", "Guest")
# 悪い例: render_template_string(f"<h1>Hello {user_name}</h1>")
# 良い例: 変数は引数として渡す(Jinja2は自動的にこれらを安全にエスケープする)
return render_template_string("<h1>Hello {{ name }}</h1>", name=user_name)
Node.js (EJS) の場合
Node.js環境でも同様だ。テンプレートエンジン側のオプションで、サンドボックス機能を有効にするか、あるいはユーザー入力をテンプレートのロジックに一切関与させない設計が必要だ。
// EJSの例:出力エスケープを徹底する
// <%= %> を使い、 <%- %> (エスケープ無効化) は禁忌とする
// また、テンプレートのパスをユーザー入力で動的に変更しないこと
app.get('/render', (req, res) => {
const templateName = req.query.page; // ユーザー入力でテンプレートを指定させない!
// 悪い例: res.render(templateName)
// 良い例: ホワイトリストで許可されたテンプレートのみ描画する
const allowedTemplates = ['home', 'about'];
if (allowedTemplates.includes(templateName)) {
res.render(templateName);
} else {
res.status(403).send("Invalid template");
}
});
—
インフラ層でのダメ押し:WAFと権限設計
開発コードだけでなく、インフラ側でも「多層防御」を敷く。これが我々プロの仕事だ。
WAFによるフィルタリング(Nginx/ModSecurity)
SSTIで頻出する文字列(__class__, __globals__, {{ など)をWAFで遮断する設定を入れておく。
# Nginx + ModSecurityのルール例
# {{ や }} が連続する不審なリクエストをブロックする
SecRule ARGS "{{.*}}" "id:10001,phase:2,deny,status:403,msg:'Potential SSTI detected'"
クラウドIAMによる「爆発範囲」の制限
万が一、SSTIでRCEを許してしまったとしても、被害を最小限に抑えるための権限設計が重要だ。
- コンテナの読み取り専用化: アプリが動作するコンテナのファイルシステムを
read-onlyにし、攻撃者が悪意のあるスクリプトを書き込めないようにする。 - 最小権限の原則: アプリが実行されるIAMロールには、S3バケットへのアクセス権限を絞り込み、メタデータサービス(
169.254.169.254)へのアクセスも極力制限する。
—
まとめ:脆弱性に「特効薬」はない
SSTIは、フレームワークの利便性を追求した結果として生まれる脆弱性だ。便利な機能には必ず代償がある。
1. テンプレートエンジンにユーザー入力を渡さない。
2. 動的なテンプレート生成は避け、テンプレートファイルはデプロイ時に固定する。
3. 万が一に備え、実行環境の権限を極限まで絞り込む。
これらを守るだけで、君のプロダクトは攻撃者にとって「極めてコストが高く、狙う価値のない標的」になる。コードを書くとき、常に「この文字列は、誰が解釈するのか?」を自問自答してほしい。その問いこそが、最強のセキュリティ対策への第一歩だ。
コメント