そのテンプレートエンジン、裏口が開いていませんか?:SSTIからRCEに至る「静かなる侵入」のメカニズム
現場でコードをレビューしていると、いまだに「テンプレートエンジンを使っているから安全だ」という甘い認識に遭遇する。しかし、Jinja2やThymeleafといった強力なツールは、適切に扱わなければ「攻撃者への招待状」に早変わりする。
今回は、Webアプリの盲点となりがちなSSTI(Server-Side Template Injection)について、なぜこれが「終わりの始まり」なのか、そしてどう防ぐべきかを、実戦的な視点で紐解いていこう。
—
1. SSTI:なぜテンプレートが「コマンド実行」に繋がるのか
テンプレートエンジンは、本来「静的なHTML」と「動的なデータ」を分離するためのものだ。しかし、開発者が「動的にテンプレートそのものを生成したい」と考え、ユーザー入力をテンプレート文字列として連結してしまうと悲劇が始まる。
攻撃のメカニズム
テンプレートエンジンには、ロジックを処理するための「式言語」が含まれている。攻撃者はこの式言語を悪用し、本来アクセス不可能な内部オブジェクト(__class__や__globals__など)を辿り、OSのコマンド実行関数を呼び出す。
例えば、PythonのJinja2で以下のようなコードがあったとする。
脆弱な実装例(絶対にしてはいけない)
@app.route(“/hello”)
def hello():
name = request.args.get(“name”)
# ユーザー入力をテンプレート文字列として直接レンダリングしている
template = f”Hello {name}!”
return render_template_string(template)
もし攻撃者が ?name={{config.__class__.__init__.__globals__}} と入力したらどうなるか? アプリケーションの内部設定がすべて露呈する。さらに進めれば、os.popen('ls').read() を実行させることも容易だ。これがSSTIによるRCE(リモートコード実行)の入口である。
—
2. 現場で使える防御戦略:根本解決は「分離」のみ
脆弱性を防ぐための黄金律は、「ユーザー入力をテンプレートのロジックとして解釈させない」ことだ。
Python (Jinja2) でのセキュアな実装
テンプレート文字列を動的に生成せず、変数は常に「引数」として渡すこと。
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route(“/hello”)
def hello():
name = request.args.get(“name”, “Guest”)
# NG: render_template_string(f”Hello {name}”)
# OK: テンプレートは固定し、値をコンテキストとして渡す
return render_template_string(“Hello {{ name }}!”, name=name)
JavaScript (Node.js/EJS) の場合
Node.jsのテンプレートエンジンも同様だ。ejs.render()に直接ユーザー入力を渡してはいけない。
// セキュアな実装例
app.get(‘/render’, (req, res) => {
const userInput = req.query.name;
// テンプレートファイルはサーバー上の静的ファイルとして保持し、
// ユーザー入力はあくまで「データ」として渡すこと
res.render(‘profile’, { name: userInput });
});
—
3. 多層防御:アプリケーションの外側でもガードする
コードの修正が大前提だが、万が一の脆弱性混入に備えて、インフラ側でも「もしもの時の逃げ道」を塞いでおくべきだ。
WAFによるペイロード検知
AWS WAFなどのマネージドルールを有効化するのは当然として、SSTI特有の文字列({{ や __class__)をブロックするカスタムルールを検討しよう。
AWS WAF カスタムルール(JSON例):
{
“Name”: “Block-SSTI-Payloads”,
“Statement”: {
“ByteMatchStatement”: {
“SearchString”: “{{“,
“FieldToMatch”: { “QueryString”: {} },
“TextTransformations”: [{ “Priority”: 0, “Type”: “URL_DECODE” }]
}
}
}
最小権限の原則(IAM・コンテナ)
アプリケーションが実行されているコンテナには、必要最低限の権限しか与えてはならない。
- 読み込み専用のファイルシステム: コンテナのルートディレクトリを読み取り専用にし、悪意あるスクリプトの書き込みを防ぐ。
- 実行権限の制限: コンテナ内での
curlやwgetの実行を制限し、外部からの追加ペイロードダウンロードをブロックする。
—
最後に:セキュリティは「疑うこと」から始まる
SSTIの恐ろしさは、開発者が「便利機能」として意図的に実装してしまうケースが多い点にある。コードを書く際、ふと立ち止まって自問してほしい。
「この入力値は、テンプレートの構造を変えてしまう可能性はないか?」
「動的なテンプレート生成」が必要な場面は、実は極めて稀だ。もし必要だとしても、それはサンドボックス化された安全な環境で処理するか、そもそも別の設計(JSONによるデータ連携など)に切り替えるべきだ。
セキュリティは、魔法のようなツール一つで完結するものではない。日々の泥臭いレビューと、徹底した「境界線の管理」こそが、あなたのシステムを守る唯一の盾となる。
さあ、今すぐコードベースを検索して render_template_string や ejs.render がどう使われているか確認してみてほしい。その一行が、明日誰かの侵入口になるかもしれないのだから。
コメント