【SSTI編】入力をテンプレートとして実行させない設計|CTF思考フレームワーク #33
こんにちは、アンペンです!
前回は、応答差からビット単位で情報を抜くブラインドSQLiを扱いました。
今回は、#14で扱ったSSTIをサーバ側のフレームワーク視点で深掘りしていきます。Jinja2/Twig/FreeMarkerなどの『テンプレ評価』からRCEに到達する経路と、その守り方を学びましょう。

SSTIは#14でやったよね?何が違うの?

#14は概念中心。今回はサーバ側でフレームワークごとの脱出経路と、サンドボックスの限界まで踏み込むよ。
SSTIは#14でも学びました。今回はその“サーバ実装編”——FlaskやSymfonyといった実際のフレームワークで、どんな書き方が地雷になるのかを見ていきます。#14で『テンプレートは式も実行できる』と知りました。今日はさらに踏み込んで、「なぜサンドボックス(安全柵)に頼ってはいけないのか」「コードのどの一行が危ないのか」まで、議事録の雛形のたとえで具体的に整理します。
テンプレートエンジンは『穴埋め』だけでなく『式評価』も持ちます。利用者入力をテンプレ文字列に混ぜると、Jinja2/Twig/FreeMarker等でRCEに到達する経路が成立します。守りは『テンプレートは固定、入力は変数』『テンプレ文字列の動的生成を禁止』が中心です。
この記事で分かること
- テンプレ評価が『コード評価』に等しい理由
- フレームワークごとの代表(Jinja2/Twig/FreeMarker/ERB)
- 『テンプレ固定+入力変数化』の徹底方法
📖 はじめてのWebセキュリティ #33|SSTI編(サーバ視点)
『テンプレが式を評価する』性質を、フレームワーク別に整理します。 シリーズ一覧を見る →
⚠️ 大事なお約束
この記事の確認は、CTF・自分の検証環境のみで行ってください。本物のサービスへのSSTIテストは不正アクセスや業務妨害に該当する可能性があります。
テンプレ評価は『コード評価』に等しい
テンプレートエンジンは、テンプレ文字列に『式の評価』を含みます。Jinja2の {{ ... }}、Twigの {{ ... }}、FreeMarkerの ${...} など。これらの中は、計算・関数呼び出し・オブジェクトアクセスが可能です。
つまり、利用者入力をテンプレ文字列の一部にしてしまうと、入力した式がそのまま評価される状態が生まれます。これがSSTIの核です。
実際の攻撃は、ここから段階的に深まります。まず {{ 7*7 }} で「お、式が評価されたな」と確認し、次に言語の内部オブジェクトをたどっていく。Pythonなら、ある入り口(__class__ など)から芋づる式にたどれば、最後はOSコマンドの実行にまで届いてしまう。最初は無害な掛け算でも、“式が動く”という事実そのものが、RCEへの一本道の入口になる。だから「計算くらい平気」とは言えないんです。
おさらいすると、テンプレートエンジンの {{ ... }} の中は、ただの“穴埋め”ではなく“式の評価”でした。{{ 7*7 }} と書けば 49 になる。つまり、テンプレ文字列に書ける=計算や関数呼び出しができる、ということ。ここに利用者の入力がテンプレートとして紛れ込めば、その入力は“ただの文字”ではなく“実行される式”として扱われてしまう。テンプレ評価はコード評価——この一点が、すべての出発点です。
図解:値として埋め込む vs テンプレ文字列に混ぜる

同じ入力でも、テンプレに『値として渡す』のと『テンプレ文字列として混ぜる』のでは、結末が大きく違います。

議事録の雛形に『参加者: {{name}}』と書いてあるとします。 nameに『山田太郎』と書く分には穴埋めですが、もし雛形そのものを誰でも書き換え可能にしてしまえば、『金庫の鍵を開けて中身を貼って』のような指示も入れられます。SSTIは、まさにこの『雛形を書き換えられた』状態です。
ここで覚える用語:サンドボックス脱出
テンプレートエンジンのサンドボックス(安全制限機構)を抜けて、システムコールやファイルアクセスに到達することです。Jinja2の __class__ 経由でPythonランタイムへアクセスする手口などが知られています。サンドボックスは『安心の道具』ではなく『気休め』程度に捉えるのが現実的です。
ここで多くの人がすがりたくなるのが『サンドボックス(安全柵)』です。テンプレートエンジンには、危ない機能を制限する柵が用意されていることがあります。でも、過信は禁物。柵というのは“この道は通さない”という個別の禁止の積み重ねで、研究者たちは何度も“柵の隙間”を見つけてきました。一個でも抜け道が残れば、そこから外へ出られてしまう。だからサンドボックスは“最後の気休め”くらいに考え、本命は別の守り(後述)に置くべきなんです。
フレームワーク別の代表例
テンプレートエンジンは言語ごとにいろいろありますが、SSTIの本質はどれも同じです。Python(Jinja2)、PHP(Twig)、Java(FreeMarker)、Ruby(ERB)——名前も文法も違いますが、共通するのは「テンプレ文字列に利用者入力が混ざると、式として評価される」こと。だから“どのエンジンを使っているか”より、“入力をテンプレ文字列に混ぜていないか”を見るのが大事です。
注意すべきエンジン

- Python Jinja2: Flask等で多用、
render_template_stringに利用者入力を渡す箇所が典型 - PHP Twig: Symfony等、サンドボックス越えのRCE報告あり
- Java FreeMarker: 古い設定では
new()などからRCE - Ruby ERB: 動的テンプレート文字列を eval する箇所が危険
共通点は、いずれも『テンプレ文字列を実行時に組み立てる』『そこに利用者入力が混じる』というパターンです。コード上で render_template_string、 Twig_Loader_Array、 Template(input) といった『動的テンプレ』APIが入っていないかが重要なチェックポイントになります。
ここで、コードレビューの“合言葉”を一つ。render_template_string という関数名(や、それに相当する各言語のAPI)を見かけたら、必ず立ち止まってください。これは『文字列を、その場でテンプレートとして実行する』機能で、SSTIのほぼすべてがここから生まれます。普通の render_template(固定ファイルを読む)なら安全。違いは“string”の一語だけ。この小さな差を見抜けるかが、分かれ目になります。
練習は、自分のコードで“動的にテンプレートを作っている場所”を探すこと。render_template_string のようなAPIに、外部入力が流れ込んでいないかをたどります。攻撃ではなく棚卸しです。見つかったら、固定ファイル+変数渡しに直せるか検討しましょう。もちろん本物のサービスに {{ 7*7 }} を送るのは厳禁、自分のラボの中だけで。
CTFでやってみよう:テンプレ評価APIを点検
自分のラボで、テンプレートエンジンの利用箇所を整理しよう
目的は実際の攻撃ではなく、『テンプレ文字列に動的に値を混ぜている箇所』を可視化することです。
- コードで
render_template_string/Twig_Loader_Array/new Template(input)等を検索する - テンプレ文字列が外部入力から組み立てられていないか確認する
- テンプレを固定ファイルに置き、利用者入力は変数として渡す形に修正する
- autoescape等のオプションが有効か、フレームワーク仕様を確認する
- WAFやログで
{{系の異常入力を検知できるようにする
守りの本命は、サンドボックスでも入力チェックでもありません。

じゃあ、入力から {{ を消したり、サンドボックスを固めたりすればいいの?

どちらも“次善策”止まりなんだ。記号を消す方式は別の書き方で抜けられるし、サンドボックスは隙間が見つかる。本命はもっとシンプルで、『テンプレートは固定、利用者入力は変数』。つまり、ひな形(テンプレ)はあらかじめ用意した決まったファイルだけにして、利用者の文字は“穴を埋める材料”として渡す。文章そのものを利用者に書かせない。そうすれば、そもそも式として評価される余地が生まれない。SSTIと戦うのではなく、戦いが起きない形にするんだよ。
守る側なら、「テンプレ固定+入力変数化+動的テンプレ禁止」
SSTIの守りは、「テンプレートは固定ファイルに置く」「利用者入力は必ず変数として渡す」「動的テンプレ生成APIを使わない」の3点が中心です。サンドボックスは補助でしかありません。
- テンプレ文字列はソース管理し、動的な組み立てを禁止する
- 利用者入力は変数(コンテキスト)として渡す(
render_template('a.html', name=input)) render_template_string等の動的テンプレAPIを利用箇所を棚卸しし、置換- autoescapeを有効にしてエスケープ漏れを最小化する
- 管理画面でテンプレ編集を許す場合は別ロール+レビュー+サンドボックスの多層化
- SSTI兆候(
{{系入力)を監視ログで検知する

『動的にテンプレを作らない』だけで、ほぼ防げるんだね。

そう。テンプレは固定、入力は変数。境界線を絶対に崩さないのが基本だよ。
ここまでをひと言で言うと、SSTI対策は『テンプレは固定、入力は変数』。#14でも出た合言葉ですが、サーバ実装でも結論は同じです。サンドボックスや記号フィルターで“出てきた攻撃を止める”のではなく、“テンプレ文字列に入力を混ぜない”という構造で、攻撃が生まれる前に断つ。これが、どのフレームワークでも変わらない王道です。
まとめ:テンプレと入力の境界を死守する
- テンプレ評価はコード評価。入力をテンプレに混ぜるとRCE
- Jinja2/Twig/FreeMarker/ERBで実例が知られている
- 守りはテンプレ固定+変数渡し+動的テンプレ禁止
- サンドボックスは過信せず、構造で防御する
今日の持ち帰りは『“string”の一語に気をつける』です。render_template と render_template_string——たった一語の違いが、安全とRCEを分けます。ひな形は固定ファイルに置き、利用者の文字は必ず“変数”として渡す。この一線さえ守れば、SSTIという大きな穴は、フレームワークを問わずしっかり塞げます。
次回は、ライブラリやコンテナイメージといったサプライチェーンから狙われる攻撃を扱います。依存関係に潜む脅威と、その守り方を見ていきましょう。
