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

【SSTI編】テンプレートに混ざる悪意とRCEへの道|CTF思考フレームワーク #14

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

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

前回は、仕様の前提を突くビジネスロジック脆弱性を扱いました。

今回は、画面やメールの「ひな形」に値を埋め込む処理が、攻撃者の手で「実行コード」に変わるSSTI(サーバサイドテンプレートインジェクション)を見ていきます。テンプレートに混ざる悪意とRCEへの道を、優しく見ていきましょう。

テンプレートって、メールとかページの『ひな形』だよね?それでRCEになるってどういうこと?

テンプレートエンジンは『穴埋め』のついでに『式を評価する』機能も持っているんだ。利用者入力をテンプレートとして渡してしまうと、その『式を評価』が悪意のコードを実行してしまう。

メールの「〇〇様、いつもありがとうございます」みたいな“ひな形”。あれが、まさかサーバ乗っ取りの入口になるなんて、ちょっと想像しにくいですよね。でも、テンプレート(ひな形)の仕組みを少し知ると、その理由がストンと腑に落ちます。カギは『テンプレートエンジンは、穴埋めのついでに“計算”もしてしまう』という性質。今日はそこを、穴埋め式の書類にたとえながら、やさしくほどいていきます。

まず結論

SSTIは、利用者入力をテンプレートエンジンに「テンプレート自体」として渡すと発生し、最終的にRCE(任意コード実行)に到達することがあります。守り方の核は「利用者入力をテンプレート文字列として絶対に渡さない」ことです。

この記事で分かること

  • テンプレートエンジンが評価する「式」と「値」の違い
  • SSTIが成立する典型ケース(メール件名・通知本文・ページレンダリング)
  • 守り側の基本姿勢(入力を式として渡さない)
難易度:すこし慣れてから 所要時間:10分 体験:テンプレ評価の挙動を見る おすすめ:#13の後

📖 はじめてのWebセキュリティ #14|SSTI編
『穴埋めのつもり』が『コード実行』に化ける構造を、優しく学びます。 シリーズ一覧を見る →

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

テンプレートエンジンは「式」を評価する仕組み

Webサイトやメール本文の多くは、テンプレートエンジン(Jinja2・Twig・ERB・FreeMarkerなど)で「ひな形に値を埋め込む」処理で作られています。たとえば こんにちは、{{ name }} さん のような形式です。

このとき {{ ... }} の中身はただの「変数の差し込み」ではなく、式として評価されます。利用者入力をテンプレート文字列に組み込んでしまうと、攻撃者は式の中で計算・関数呼び出し・サーバ内のオブジェクトアクセスを実行できてしまいます。

ここが、安全と危険の分かれ道です。同じ利用者の入力でも、『name という変数の“中身”として渡す』なら、ただの文字として表示されるだけで安全。でも、その入力が『テンプレートの文章そのもの』として渡されてしまうと、入力に含まれた {{ ... }} が式として実行されてしまう。“どこに置かれたか”だけで、ただの文字が一転して命令に化ける——これがSSTIの核心です。

まず、テンプレートの {{ name }} という書き方を見てみましょう。これは「ここに name の中身を入れてね」という穴埋め指示です。ここまではイメージ通り。ところがテンプレートエンジンは、この二重カッコの中を“ただ差し込む”のではなく、“計算して結果を出す”力を持っています。たとえば {{ 1 + 1 }} と書けば、ちゃんと 2 になる。つまり中身は『値』ではなく『式』として読まれているんです。

図解:『値』として埋め込む vs 『式』として評価される

テンプレートに値として渡す場合と、ユーザ入力をテンプレ自体として渡してSSTIになる場合の比較図
テンプレ文字列にユーザ入力を混ぜると、式として評価されてしまう。

同じ入力でも、テンプレートに渡される位置によって「ただの文字列」になるか「式」になるかが変わります。

穴埋め欄に「金庫を開けて中身を渡す」と書かれたら受け取った人が実行してしまうたとえ図
穴埋め欄に『指示』を書かれると、システムは実行してしまう。SSTIも同じ構造。
📄 たとえるなら、穴埋め式の社内資料

「お名前: ____」と書かれた紙の穴埋め欄に名前を書くのは普通の使い方です。でも、その紙自体が「ここに書かれた指示通りに動く」というルールだったらどうでしょう。穴埋め欄に「金庫を開けて中身を渡す」と書かれてしまえば、受け取った人はその指示を実行してしまいます。SSTIは、テンプレートに「式を実行する力」があることを忘れた瞬間に起こります。

ここで覚える用語:SSTI
Server-Side Template Injectionの略です。利用者入力がテンプレートエンジンに「テンプレートとして」渡されてしまうことで、サーバ側で任意のコードが評価される脆弱性です。最終的にRCE(任意コード実行)に至るケースが多く、影響が非常に大きい部類に入ります。

『でも、式が計算できるだけなら、せいぜい足し算くらいでは?』と思うかもしれません。ところが、テンプレートエンジンの“式”は、サーバ内部のオブジェクトや関数にも手を伸ばせることが多いんです。計算から始まって、設定値の読み出し、ファイル操作、最後にはOSコマンドの実行——と、芋づる式に深いところへ届いてしまう。だからSSTIは「ただの表示崩れ」では済まず、RCE(任意コード実行)という最悪のゴールに化けるんです。

SSTIが成立する代表的な場面

SSTIが生まれやすいのは、3つの場面です。『メールの件名や本文』『利用者が編集できるお知らせ・挨拶文』『管理画面の帳票テンプレ』。どれも“利用者やスタッフが文章そのものを書ける”のが共通点。便利な「自由に編集できる」機能ほど、テンプレと入力の境界が溶けやすいんです。

よくある混入経路

メール本文・動的ページ生成・レポート/帳票テンプレの3つのSSTI発生場面を示したカード型インフォグラフィック
①メール本文 ②動的ページ生成 ③帳票テンプレ。守りは『テンプレは固定・入力は変数』の境界維持。
  • メール件名・本文:利用者の入力(ニックネームなど)をテンプレートそのものに埋め込んで送信する
  • 動的ページ生成:「カスタム挨拶文」「お知らせ本文」などをユーザが直接編集できる機能
  • レポート・帳票テンプレ:管理画面で雛形を編集できる機能で、エスケープを忘れる

共通点は、「利用者入力がそのままテンプレート文字列の一部になる」ことです。render_template_string(user_input)のような形が典型的に危険です。

実は、SSTIがあるかどうかを確かめる“魔法の合言葉”のような入力があります。それが {{ 7*7 }}。もし画面に 49 と表示されたら、それは入力が式として計算された動かぬ証拠です。ただの文字として扱われていれば {{ 7*7 }} のまま出るはずですからね。この一手で“境界が崩れているか”が一目で分かるので、診断の世界では定番の確認方法になっています(もちろん、試すのは自分の環境だけ、ですよ)。

では、さっきの {{ 7*7 }} を自分のラボで試してみましょう。49 が出るかどうかを見るだけです。出れば“式が評価されている”サイン。ここでも鉄則は同じで、本物のサービスで試すのは厳禁です。とくにSSTIはRCEに直結しうるので、確認は必ず自分の演習環境の中だけにしてください。

CTFでやってみよう:式が評価されるか観察する

やってみよう / 演習環境限定

自分の検証環境で、テンプレ評価の挙動を確認しよう

目的は本物のサービスを攻撃することではなく、「自分のラボでテンプレートが入力をどう扱っているか」を見ることです。

  1. CTFや自分の検証環境のテンプレートを使う機能(メール本文プレビューなど)を開く
  2. 普通の文字列(例: hello)を入力して、画面に出るか確認する
  3. 計算式(例: {{ 7*7 }})を入力して、結果が49になるか観察する
  4. 結果が 49 なら、それは「式が評価された」サイン
  5. サーバ側コードで render_template_stringeval 系の関数が使われていないか確認する
攻撃用のサンドボックス脱出テクニックを本物のサービスで試すのは絶対にやめてください。確認は自分のラボ・自分のテストデータで完結させます。

守り方はシンプルですが、よくある“勘違い”があるので、先に潰しておきましょう。

危ない関数をサンドボックス(隔離)で囲えば、それで安全じゃないの?

気持ちは分かるけど、サンドボックスは“最後の保険”くらいに考えたほうがいい。テンプレートエンジンのサンドボックスは、過去に何度も“脱出”される手口が見つかってきたんだ。だから本命の守りは、そもそも『利用者入力をテンプレート文字列に混ぜない』こと。入力は必ず“変数”として渡す。この境界さえ守れば、サンドボックスに頼る場面そのものがほとんどなくなるよ。

守る側なら、「テンプレ自体に利用者入力を入れない」を徹底

SSTIの守りは、「テンプレート文字列にユーザ入力を絶対に混ぜない」「常に変数として渡す」の2点が基本です。サンドボックス機能は補助でしかなく、過信しないことが大切です。

守るための基本チェック
  • テンプレート文字列はソース管理し、動的に組み立てない
  • 利用者入力は必ず変数として渡し、エスケープを通す
  • render_template_string など、文字列をテンプレートとして評価する関数の利用箇所を洗い出す
  • 管理画面でテンプレ編集を許す場合は、別ロール・別レビュー・サンドボックスの3段階で守る
  • エンジン側のautoescape機能を有効にし、エスケープ漏れを最小化する
  • SSTIが疑われる挙動はWAFや監査ログで検知できるようにする

テンプレに『直接』入力を混ぜなければ、ほぼ防げるんだね。

そう。テンプレートは固定、利用者入力は変数。この境界線を絶対に崩さないのがSSTI対策の基本だよ。

ここまでをひと言で言うと、SSTI対策は『テンプレは固定、入力は変数』。この一線を引き続けることが、すべての守りの土台です。ひな形(テンプレート)はあらかじめ用意した決まった文章だけにして、利用者が入れる文字は“穴を埋める材料”として渡す。文章そのものを利用者に書かせない——そう決めておけば、式が暴走する余地はなくなります。

まとめ:テンプレと入力の「境界線」を守る

今回のポイント
  • テンプレートエンジンは『穴埋め』+『式評価』の機能を持つ
  • 利用者入力をテンプレート文字列自体に混ぜるとSSTI
  • 守りは「テンプレは固定」「入力は変数」「render_template_stringを使わない」が基本
  • サンドボックスは補助、過信しない

今日の持ち帰りは『穴を埋めさせても、ひな形は書かせない』です。テンプレートはとても便利な仕組みですが、“式を実行する力”を内に秘めています。その力を利用者の手に渡さないこと——つまり入力を必ず変数として扱うこと。これだけで、SSTIという大きな穴はほぼ塞げます。

次回は、信頼できないデータを「オブジェクト」として復元するときに起きるデシリアライズを扱います。蘇らせてはいけないデータの罠を見ていきましょう。

次に読みたい記事

参考資料

記事URLをコピーしました