PR 本記事には広告(Amazonアソシエイト・もしもアフィリエイト・A8.net等)が含まれます。掲載情報の正確性には努めていますが、商品の詳細は必ずリンク先で最新情報をご確認ください。
CTF・セキュリティ学習

【SSTI編】入力をテンプレートとして実行させない設計|CTF思考フレームワーク #33

かも次郎とアンペンが「SSTI」を解説するマスコットイラスト
安全に生きたい編集部

こんにちは、アンペンです!

前回は、応答差からビット単位で情報を抜くブラインドSQLiを扱いました。

今回は、#14で扱ったSSTIをサーバ側のフレームワーク視点で深掘りしていきます。Jinja2/Twig/FreeMarkerなどの『テンプレ評価』からRCEに到達する経路と、その守り方を学びましょう。

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

#14は概念中心。今回はサーバ側でフレームワークごとの脱出経路と、サンドボックスの限界まで踏み込むよ。

SSTIは#14でも学びました。今回はその“サーバ実装編”——FlaskやSymfonyといった実際のフレームワークで、どんな書き方が地雷になるのかを見ていきます。#14で『テンプレートは式も実行できる』と知りました。今日はさらに踏み込んで、「なぜサンドボックス(安全柵)に頼ってはいけないのか」「コードのどの一行が危ないのか」まで、議事録の雛形のたとえで具体的に整理します。

まず結論

テンプレートエンジンは『穴埋め』だけでなく『式評価』も持ちます。利用者入力をテンプレ文字列に混ぜると、Jinja2/Twig/FreeMarker等でRCEに到達する経路が成立します。守りは『テンプレートは固定、入力は変数』『テンプレ文字列の動的生成を禁止』が中心です。

この記事で分かること

  • テンプレ評価が『コード評価』に等しい理由
  • フレームワークごとの代表(Jinja2/Twig/FreeMarker/ERB)
  • 『テンプレ固定+入力変数化』の徹底方法
難易度:中級向け 所要時間:10分 体験:テンプレ評価APIを点検 おすすめ:#32の後

📖 はじめてのWebセキュリティ #33|SSTI編(サーバ視点)
『テンプレが式を評価する』性質を、フレームワーク別に整理します。 シリーズ一覧を見る →

⚠️ 大事なお約束
この記事の確認は、CTF・自分の検証環境のみで行ってください。本物のサービスへのSSTIテストは不正アクセスや業務妨害に該当する可能性があります。

テンプレ評価は『コード評価』に等しい

テンプレートエンジンは、テンプレ文字列に『式の評価』を含みます。Jinja2の {{ ... }}、Twigの {{ ... }}、FreeMarkerの ${...} など。これらの中は、計算・関数呼び出し・オブジェクトアクセスが可能です。

つまり、利用者入力をテンプレ文字列の一部にしてしまうと、入力した式がそのまま評価される状態が生まれます。これがSSTIの核です。

実際の攻撃は、ここから段階的に深まります。まず {{ 7*7 }} で「お、式が評価されたな」と確認し、次に言語の内部オブジェクトをたどっていく。Pythonなら、ある入り口(__class__ など)から芋づる式にたどれば、最後はOSコマンドの実行にまで届いてしまう。最初は無害な掛け算でも、“式が動く”という事実そのものが、RCEへの一本道の入口になる。だから「計算くらい平気」とは言えないんです。

おさらいすると、テンプレートエンジンの {{ ... }} の中は、ただの“穴埋め”ではなく“式の評価”でした。{{ 7*7 }} と書けば 49 になる。つまり、テンプレ文字列に書ける=計算や関数呼び出しができる、ということ。ここに利用者の入力がテンプレートとして紛れ込めば、その入力は“ただの文字”ではなく“実行される式”として扱われてしまう。テンプレ評価はコード評価——この一点が、すべての出発点です。

図解:値として埋め込む vs テンプレ文字列に混ぜる

テンプレファイルに変数で値を渡す安全なパターンと、テンプレ自体にユーザ入力を混ぜてSSTIになる比較図
テンプレ評価=コード評価。境界を崩すと最終的にRCEへ到達する。

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

議事録テンプレに普通に記入する場合と、雛形自体に指示が書き加えられる場合のたとえ図
雛形そのものを書き換えられると、指示通り動いてしまう。SSTIも同じ。
📑 たとえるなら、議事録作成テンプレ

議事録の雛形に『参加者: {{name}}』と書いてあるとします。 nameに『山田太郎』と書く分には穴埋めですが、もし雛形そのものを誰でも書き換え可能にしてしまえば、『金庫の鍵を開けて中身を貼って』のような指示も入れられます。SSTIは、まさにこの『雛形を書き換えられた』状態です。

ここで覚える用語:サンドボックス脱出
テンプレートエンジンのサンドボックス(安全制限機構)を抜けて、システムコールやファイルアクセスに到達することです。Jinja2の __class__ 経由でPythonランタイムへアクセスする手口などが知られています。サンドボックスは『安心の道具』ではなく『気休め』程度に捉えるのが現実的です。

ここで多くの人がすがりたくなるのが『サンドボックス(安全柵)』です。テンプレートエンジンには、危ない機能を制限する柵が用意されていることがあります。でも、過信は禁物。柵というのは“この道は通さない”という個別の禁止の積み重ねで、研究者たちは何度も“柵の隙間”を見つけてきました。一個でも抜け道が残れば、そこから外へ出られてしまう。だからサンドボックスは“最後の気休め”くらいに考え、本命は別の守り(後述)に置くべきなんです。

フレームワーク別の代表例

テンプレートエンジンは言語ごとにいろいろありますが、SSTIの本質はどれも同じです。Python(Jinja2)、PHP(Twig)、Java(FreeMarker)、Ruby(ERB)——名前も文法も違いますが、共通するのは「テンプレ文字列に利用者入力が混ざると、式として評価される」こと。だから“どのエンジンを使っているか”より、“入力をテンプレ文字列に混ぜていないか”を見るのが大事です。

注意すべきエンジン

Python Jinja2・PHP Twig・Java FreeMarker・Ruby ERBの4つのテンプレートエンジンの注意点を示したカード型インフォグラフィック
守りは『テンプレ固定+変数渡し+動的テンプレ禁止』。
  • Python Jinja2: Flask等で多用、 render_template_string に利用者入力を渡す箇所が典型
  • PHP Twig: Symfony等、サンドボックス越えのRCE報告あり
  • Java FreeMarker: 古い設定では new() などからRCE
  • Ruby ERB: 動的テンプレート文字列を eval する箇所が危険

共通点は、いずれも『テンプレ文字列を実行時に組み立てる』『そこに利用者入力が混じる』というパターンです。コード上で render_template_stringTwig_Loader_ArrayTemplate(input) といった『動的テンプレ』APIが入っていないかが重要なチェックポイントになります。

ここで、コードレビューの“合言葉”を一つ。render_template_string という関数名(や、それに相当する各言語のAPI)を見かけたら、必ず立ち止まってください。これは『文字列を、その場でテンプレートとして実行する』機能で、SSTIのほぼすべてがここから生まれます。普通の render_template(固定ファイルを読む)なら安全。違いは“string”の一語だけ。この小さな差を見抜けるかが、分かれ目になります。

練習は、自分のコードで“動的にテンプレートを作っている場所”を探すこと。render_template_string のようなAPIに、外部入力が流れ込んでいないかをたどります。攻撃ではなく棚卸しです。見つかったら、固定ファイル+変数渡しに直せるか検討しましょう。もちろん本物のサービスに {{ 7*7 }} を送るのは厳禁、自分のラボの中だけで。

CTFでやってみよう:テンプレ評価APIを点検

やってみよう / 自分の環境・CTFのみ

自分のラボで、テンプレートエンジンの利用箇所を整理しよう

目的は実際の攻撃ではなく、『テンプレ文字列に動的に値を混ぜている箇所』を可視化することです。

  1. コードで render_template_string / Twig_Loader_Array / new Template(input) 等を検索する
  2. テンプレ文字列が外部入力から組み立てられていないか確認する
  3. テンプレを固定ファイルに置き、利用者入力は変数として渡す形に修正する
  4. autoescape等のオプションが有効か、フレームワーク仕様を確認する
  5. WAFやログで {{系の異常入力を検知できるようにする
本物のサービスにSSTIペイロードを送る試行は絶対にやめてください。確認は自分のラボの範囲だけです。

守りの本命は、サンドボックスでも入力チェックでもありません。

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

どちらも“次善策”止まりなんだ。記号を消す方式は別の書き方で抜けられるし、サンドボックスは隙間が見つかる。本命はもっとシンプルで、『テンプレートは固定、利用者入力は変数』。つまり、ひな形(テンプレ)はあらかじめ用意した決まったファイルだけにして、利用者の文字は“穴を埋める材料”として渡す。文章そのものを利用者に書かせない。そうすれば、そもそも式として評価される余地が生まれない。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_templaterender_template_string——たった一語の違いが、安全とRCEを分けます。ひな形は固定ファイルに置き、利用者の文字は必ず“変数”として渡す。この一線さえ守れば、SSTIという大きな穴は、フレームワークを問わずしっかり塞げます。

次回は、ライブラリやコンテナイメージといったサプライチェーンから狙われる攻撃を扱います。依存関係に潜む脅威と、その守り方を見ていきましょう。

次に読みたい記事

参考資料

記事URLをコピーしました