SSTI(サーバーサイドテンプレートインジェクション)の深淵:なぜ「テンプレートエンジン」が最大の脆弱性になるのか
やあ、現場の最前線でコードと格闘しているエンジニア諸君。
インジェクション攻撃といえば、SQLiやOSコマンドインジェクションを連想するだろう。しかし、現代のWebアプリケーションにおいて、それらと同等、あるいはそれ以上に「事故」りやすく、かつ被害が深刻化しやすいのが SSTI(Server-Side Template Injection) だ。
多くのエンジニアは「フレームワークがよしなにエスケープしてくれるから大丈夫」と高を括っている。だが、その「よしなに」がどこまでカバーし、どこからが「テンプレートエンジンの仕様(=悪用可能な機能)」なのかを理解している者は驚くほど少ない。
今日は、テンプレートエンジンという「諸刃の剣」を正しく扱い、脆弱性を根絶するための設計思想を叩き込む。
—
1. なぜサンドボックスは「穴」だらけなのか
まず認識を改めてほしい。Jinja2(Python)やTwig(PHP)、EJS(Node.js)といった強力なテンプレートエンジンは、本来「ドキュメントをレンダリングするためのもの」ではない。「テンプレート内で任意のコードを実行するための小さなプログラミング言語」なんだ。
攻撃者は、テンプレートエンジンが提供する「オブジェクトのメタデータにアクセスする機能」を悪用する。
攻撃者の視点:PoC(Jinja2の例)
例えば、ユーザー入力をそのままテンプレートに埋め込むという、初歩的なミスがあったとしよう。
脆弱な実装例
template = “Hello, ” + user_input
return render_template_string(template)
攻撃者は user_input に以下のようなペイロードを送り込む。
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
これだけで、OSコマンドが実行され、サーバーの権限情報が丸見えになる。テンプレートエンジンは「安全なサンドボックス」を提供しようと努力しているが、Pythonのような動的言語では、オブジェクトの継承関係を辿れば、いとも簡単に「サンドボックスの外」へ脱出できてしまうのだ。
—
2. 鉄則:ロジックとビューの完全分離
SSTIを防ぐための最大の防御策は、「ユーザー入力をテンプレートの構造として扱わせない」ことだ。これに尽きる。
テンプレート変数は、あくまで「表示するデータ」としてのみ渡すべきだ。決して、テンプレートの文字列自体を動的に構築してはならない。
安全な実装例(Python / Flask + Jinja2)
間違っても render_template_string にユーザー入力を連結して渡してはいけない。常にテンプレートファイルを固定し、コンテキストとしてデータを注入する。
from flask import Flask, render_template
app = Flask(__name__)
@app.route(“/profile”)
def profile():
# ユーザー入力は変数としてのみ扱う(安全)
user_name = request.args.get(“name”, “Guest”)
# テンプレートは固定のファイルを指定する
# 変数は必ず辞書形式で渡す。テンプレートエンジン側はこれをただの文字列として処理する
return render_template(“profile.html”, name=user_name)
— profile.html —
Hello, {{ name }}!
Jinja2はここで自動的にエスケープを行い、HTMLタグなどを無害化してくれる
—
3. 実践:Node.js (EJS) における防御のTips
JavaScript環境でのテンプレートエンジンも同様だ。特にExpress環境で ejs を使う場合、<%= を使うか <%- を使うかで運命が決まる。
<%=: HTMLエスケープを行う(推奨)<%-: エスケープせずそのまま出力する(XSSおよびSSTIの温床)
もし、どうしてもHTMLをレンダリングしたい場合は、サーバーサイドで信頼できるライブラリ(dompurifyなど)を通した後にテンプレートへ渡すこと。テンプレートエンジン側にロジックを任せてはいけない。
---
4. インフラ側で「最後の砦」を築く
アプリ側の修正が追いつかない場合や、多層防御としてインフラ側で対策を講じるなら、WAFによるパターンマッチングが有効だ。
Nginx/ModSecurity 向けの設定ヒント(概略)
攻撃者は {{, {%, __class__, __mro__ といったテンプレート特有の構文を多用する。これらをWAFでフィルタリングするルールを設定しておこう。
ModSecurityのルール例(簡易版)
テンプレートエンジンの特殊構文を検知してブロックする
SecRule ARGS "@rx (\{\{|\{\%|__class__|__mro__)" \
"id:1001,phase:2,deny,status:403,msg:'Potential SSTI Attack Detected'"
※ただし、これは対症療法に過ぎない。本質的な対策は、アプリケーションの設計修正にあることを忘れないでほしい。
---
最後に:プロフェッショナルとしての一言
SSTIの恐ろしさは、それが「コードの一部」として実行される点にある。SQLiのようにデータベースの中身を盗まれるだけでなく、サーバーそのものを乗っ取られ、バックドアを仕掛けられ、あなたのアプリが踏み台にされる未来が待っている。
開発の現場では、便利さとセキュリティは常にトレードオフだ。しかし、テンプレートエンジンの「動的な柔軟性」を削ぎ落とすことは、単なる制限ではなく、強固なシステムを維持するための「規律」である。
「動的にテンプレートを作る必要がある」という要件が出てきたら、一度立ち止まって考えてくれ。それは本当に必要な機能か? それとも、ただ実装をサボるための言い訳ではないか?
君たちの書くコードが、次のインシデントを防ぐ盾になることを期待している。では、また現場で会おう。
コメント