おい、そこの画面を見せてみろ。……ふむ、また新しく立ち上げたWebサービスのレビューだな。
お前たちが「モダンなフレームワークを使っているから安全だ」と信じ込んでいるその入力フォーム、本当に安全だと言い切れるか? 画面からの入力をテンプレートエンジンにそのまま流し込んで、いい感じにHTMLをレンダリングさせてドヤ顔をしている開発チームが後を絶たないが、レッドチームの視点から言わせてもらえば、あれは「どうぞサーバーのシェルを握ってください」とアタッカーに鍵を渡しているようなものだ。
今回は、ペネトレーションテストの現場で最も香ばしい成果を上げる脆弱性の一つ、SSTI(Server-Side Template Injection:サーバーサイドテンプレートインジェクション)について徹底的に叩き込んでやる。
甘いサニタイズがいかに無力か、そしてどうすれば絶対に破られない堅牢なコードが書けるのか、俺の背中を見てしっかり覚えるんだ。
—
1. SSTIとは何か? なぜ「ただの入力ミス」でサーバーが落ちるのか
まず大前提として、テンプレートエンジン(Jinja2, Thymeleaf, Twig, Velocityなど)の役割を思い出せ。こいつらは「動的なデータ」と「HTMLなどの静的なテンプレート」を合体させて画面を出力するための便利な仕組みだ。
問題は、「テンプレートの構造自体を定義するコード」の中に「ユーザーからの入力値」が直接埋め込まれてしまう瞬間に起きる。
開発者はこう考える。「{{ name }}って書けば、ユーザーが入力した名前が画面に出るんだから安全だろ?」と。
違う。それはテンプレートの「データ」として渡した場合の話だ。もし、テンプレートのソースコードそのものを組み立てる文字列結合の中にユーザー入力を混ぜ込んでいたらどうなるか?
アタッカーは {{ 7*7 }} のような数式や、言語固有のオブジェクト構造を暴くペイロードを送り込む。テンプレートエンジンはそれを「ユーザーからのデータ」ではなく、「実行すべきテンプレートのコードの一部」として解釈し、評価(Evaluation)してしまうのだ。
—
2. 【攻撃者視点】Jinja2サンドボックスの華麗なバイパスとPoC
PythonのJinja2は非常に強力だが、デフォルトでは脆弱な設定のまま放置されていることが多い。まずは、レッドチームが裏で何をやっているのか、その手口のリアルを見せておこう。
例えば、以下のような脆弱なPython (Flask) アプリがあったとする。
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route("/greeting")
def greeting():
# 最悪な実装:ユーザー入力をそのままテンプレート文字列に結合している
user_input = request.args.get("name", "Guest")
template = f"<h1>Hello, {user_input}!</h1>"
return render_template_string(template)
if __name__ == "__main__":
app.run(debug=True)
このコードに対し、俺たちが送り込む最初のプローブ(探知用ペイロード)は決まりきっている。
http://localhost:5000/greeting?name={{7*7}}
もし画面に Hello, 49! と出力されたなら、そこには確実にSSTIが存在する。計算結果が評価されてしまっている証拠だ。
サンドボックスの壁をどうブチ破るか?
「Jinja2にはサンドボックスがあるから、システムコマンドは叩けないはずだ」とたかをくくっているそこのお前、甘い。Jinja2は組み込みの危険な関数(evalやexecなど)を直接使えなくしているが、Pythonのオブジェクト指向の特性を利用したリフレクション(反射)を使えば、サンドボックスの壁など紙細工のように破れる。
実戦で使われる典型的なエクスプロイトの断片を見てみよう。
# リクエストパラメータに仕込まれる難読化されたペイロードの例
# __class__ や __mro__ を辿って、Pythonの基底クラスからosモジュールを呼び出す
{{ "".__class__.__mro__[1].__subclasses__() }}
このリクエストを投げることで、メモリ上にロードされているすべてのクラスのリストが取得できる。その中から subprocess.Popen や os モジュールに関連するインデックスを見つけ出し、最終的には以下のようなコードでリバースシェルを奪取する。
# システムコマンド(例: idコマンド)を実行させる悪意あるペイロードの概念
{{
request.application.__globals__.__builtins__.__import__('os').popen('id').read()
}}
これがSSTIの恐怖だ。入力値の検証を怠っただけで、Webアプリのプロセス権限で任意のOSコマンドが実行され、内側のネットワークへと侵入される。
—
3. 【防御側必読】安全な実装と完全サニタイズの鉄則
さて、ここからが本題だ。お前たちが明日から現場で実装すべき「絶対に破られないセキュアなコード」を提示する。
SSTIを防ぐための黄金律はたった一つ。
「ユーザー入力をテンプレートのソースコード(文字列)に絶対に含めるな。データとしてのみ渡せ」
Python (Flask / Jinja2) の場合
先ほどの脆弱なコードを、どう修正すべきか。正解はテンプレート文字列をその都度組み立てるのではなく、あらかじめ用意されたファイルテンプレートを呼び出し、入力値は純粋な「変数」として渡すことだ。
【セキュアな実装サンプル】
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/greeting")
def greeting():
# ユーザー入力を取得(この時点では単なるプレーンテキストの文字列)
user_input = request.args.get("name", "Guest")
# 【鉄則】テンプレート文字列を動的に生成せず、静的なテンプレートファイルを指定する
# Jinja2はテンプレートファイル内で変数を自動的にエスケープ(HTMLエスケープ)するため安全
return render_template("greeting.html", name=user_input)
if __name__ == "__main__":
app.run(debug=False)
対応するテンプレートファイル (templates/greeting.html) 側の書き方も重要だ。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>挨拶ページ</title>
</head>
<body>
<!-- Jinja2の {{ name }} はデフォルトでHTMLエスケープ(XSSおよびSSTI防止)が効く -->
<h1>Hello, {{ name }}!</h1>
<!-- 注意:もしどうしてもHTMLタグを許可したい場合でも、安全なサニタイザを通すこと -->
</body>
</html>
Java (Spring Boot / Thymeleaf) の場合
JavaのThymeleafでも同様だ。コントローラー側でモデルに値を詰めて渡すのが大原則だが、Thymeleafの式評価構文(__${...}__ や #exec など)を不適切に用いるとSSTIを引き起こす。
【セキュアな実装サンプル (Spring Boot)】
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class SecureController {
@GetMapping("/secure-greeting")
public String greeting(
@RequestParam(name = "name", required = false, defaultValue = "Guest") String name,
Model model) {
// 【鉄則】ユーザー入力をそのままThymeleafのテンプレート名や式として評価させない
// 単純にモデル属性として渡すことで、コンテキスト内で安全に扱われる
model.addAttribute("name", name);
// あらかじめコンパイル・配置された安全なテンプレートを指定
return "greeting_template";
}
}
対応するThymeleafのHTML側 (src/main/resources/templates/greeting_template.html):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>セキュアな挨拶</title>
</head>
<body>
<!-- th:text 属性を使用することで、テキストとして安全にレンダリングされる -->
<h1 th:text="'Hello, ' + ${name} + '!'">Hello, Guest!</h1>
<!-- 絶対に避けるべき危険な書き方(インライン評価の誤用):
<div th:utext="${name}"></div> や式の中に直接パラメータを埋め込む行為 -->
</body>
</html>
—
4. インフラ層での多層防御(WAFとコンテナセキュリティ)
アプリケーションコードの修正が基本だが、レッドチームの攻撃を完全にいなすためには、インフラ層での多層防御(ディフェンス・イン・ディプス)が不可欠だ。
1. WAF(Web Application Firewall)シグネチャの導入
SSTI特有の構文、例えば {{、}}、${、*、お馴染みのリフレクション属性(__class__ や __mro__)といったキーワードは、WAFの正規表現ルールで強力に検知・ブロックできる。ModSecurityなどのルールセットで、テンプレートインジェクションの兆候があるリクエストを弾く設定を必ず入れておけ。
2. 最小権限の原則(コンテナ・IAMの硬化)
万が一、アプリケーションにSSTIの脆弱性が残っていた最悪のシナリオを想定しろ。その時、被害を最小限に抑えるのがインフラの仕事だ。
- 実行ユーザーの制限:Webアプリのプロセスを
rootなどの特権ユーザーで絶対に動かすな。専用の低権限ユーザー(例:www-dataやappuser)に閉じ込めろ。 - ファイルシステムのReadOnly化:コンテナやサーバーのルートファイルシステムを読み取り専用(Read-Only)にし、
/tmpなどの限られた領域以外への書き込みや、外部からの悪意あるスクリプトのダウンロード・実行を物理的に防げ。
—
5. チーフエンジニアからの総括
SSTIは、単なる「入力値のエスケープ漏れ」に見えて、その実、フレームワークの内部構造やオブジェクト指向の仕組みを熟知していなければ防ぎきれない、非常に奥が深い脆弱性だ。
「動的にテンプレートを組み立てたい」という誘惑に負けて、文字列結合でコードを生成するような設計をした瞬間、お前たちのシステムは外部のハッカーの掌の上で踊らされることになる。
明日からコードレビューを行う際は、render_template_string や、それに類する動的テンプレート評価メソッドがコードベースに紛り込んでいないか、目を皿のようにして確認しろ。見つけ次第、即座に修正させろ。
セキュリティは「後付けのパッチ」ではなく、「最初の設計」で決まる。頼んだぞ。
コメント