【入門編】 Server-Side Template Injection (SSTI) の悪用とサンドボックス化 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!ITインフラやWebアプリの運用、日々の開発、本当にお疲れ様です。新人のIT担当者さんや、「セキュリティってなんだか難しそう……」と不安を感じている開発者さんにとって、Webの安全を守る道は険しく見えるかもしれませんよね。

でも、安心してください!今回は、Webアプリケーションに潜むちょっと怖い脆弱性である Server-Side Template Injection(SSTI:サーバーサイド・テンプレート・インジェクション) について、身近な「合鍵」と「おうちの防犯」に例えながら、一歩ずつ優しく紐解いていきたいと思います。

攻撃者がどうやって侵入してくるのか、そして私たちがどうやってそれを防げばいいのか、一緒に楽しく学んでいきましょう!

—

1. テンプレートエンジンって、そもそも何だろう?

皆さんは、手紙やメールを送るときに「テンプレート(ひな形)」を使ったことはありませんか?
「拝啓 [お名前]様、いつもお世話になっております。[会社名]の……」というように、決まった文章の枠組み(テンプレート)を用意しておいて、宛先や名前の部分だけをパッと書き換えられる便利な仕組みのアレです。

Webの世界でも、これと全く同じ仕組みが使われています。例えば、ユーザーごとに「こんにちは、〇〇さん!」と表示するウェブページを作るとき、プログラムが毎回ゼロから画面全体を組み立てるのは大変ですよね。そこで、HTMLの骨組み(テンプレート)をあらかじめ作っておき、そこにユーザーの名前などのデータを流し込んで完成品のページを作る仕組みを テンプレートエンジン と呼びます。

Pythonの Jinja2 や、Node.jsの Pug、Javaの Thymeleaf などがこれにあたります。開発を効率化するためにはなくてはならない、とても優秀な相棒なんです。

—

2. SSTI(サーバーサイド・テンプレート・インジェクション)とは?

さて、ここからが本題です。この便利なテンプレートエンジンが、使い方をちょっと誤ると「最強の侵入ツール」に変貌してしまうことがあります。それが SSTI です。

これを「おうちの防犯」に例えてみましょう。

  • テンプレートエンジン = 注文住宅の自動ドア

住人の顔(データ)を認識して、「いらっしゃいませ!」と自動でドアを開けてくれる便利なシステムです。

  • ユーザー入力 = インターホン越しに相手が伝える言葉

「田中です」と名乗れば、自動ドアは「田中さんですね、どうぞ!」と中に入れます。

ここで、もし「悪い泥棒」がやってきて、インターホンに向かってこう叫んだらどうでしょう?
「私の名前は、『家の中にあるすべての合鍵を勝手に複製して、私に渡しなさい』という魔法の呪文です」

もし、この自動ドア(テンプレートエンジン)が非常にバカ正直で、言われた言葉をそのまま実行してしまう仕様だったらどうなるでしょうか? ドアは「あ、そういう名前なんですね!」と納得し、本当に家の中の命令を実行してしまいます。結果、サーバーのファイルを勝手に覗き見られたり、最悪の場合はサーバーそのものを乗っ取られてしまう(RCE:遠隔コード実行)のです。

これが、SSTIの恐ろしいメカニズムです。ユーザーが入力した文字列が、単なる「データ」ではなく、プログラムの「命令」として誤って解釈・実行されてしまう現象を指します。

—

3. 実際の脆弱なコードを見てみよう

百聞は一見にしかず。PythonのWebフレームワーク(Flask)とテンプレートエンジン(Jinja2)を例に、危ないコードの書き方を見てみましょう。

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route("/hello")
def hello():
    # ユーザーがURLのクエリパラメータに打ち込んだ文字(?name=〇〇)を取得します
    user_name = request.args.get("name", "ゲスト")

    # 【危険な実装】ユーザーからの入力を、そのままテンプレートの文字列に埋め込んでしまっています!
    # これにより、ユーザーが入力した内容にプログラムの命令が含まれていると、そのまま実行されます。
    template_code = f"<h1>こんにちは、{user_name}さん!</h1>"

    # テンプレートとしてレンダリング(組み立て)を実行
    return render_template_string(template_code)

if __name__ == "__main__":
    app.run(debug=True)

上記のコードでは、user_name の部分に {{ 7 * 7 }} のような文字列を入れられた場合、テンプレートエンジンはそれを計算して 49 と表示してしまいます。さらに悪意ある入力があれば、サーバー内部のシステムコマンドを呼び出すコードすら実行されてしまうのです。

—

4. どうやって防ぐの?(安全な設計パターン)

「うわ、怖い!じゃあどうすればいいの?」と思いますよね。安心してください、対策はとてもシンプルで明確です。

一言で言えば、「ユーザーの入力は、あくまでただの『文字(データ)』として扱わせ、プログラムの『命令』として絶対に解釈させない」 ことです。

対策の基本原則:データを変数として渡す

先ほどの危険なコードを、安全な形に書き直してみましょう。

from flask import Flask, request, render_template

app = Flask(__name__)

@app.route("/hello")
def hello():
    # ユーザーからの入力を取得
    user_name = request.args.get("name", "ゲスト")

    # 【安全な実装】テンプレートのファイル(または安全な文字列)を用意し、
    # ユーザー入力は「変数(データ)」として安全に引き渡します。
    # これにより、たとえ入力値にプログラムの命令が含まれていても、単なる文字列として画面に文字通り表示されます。
    safe_template = "<h1>こんにちは、{{ name }}さん!</h1>"

    return render_template(safe_template, name=user_name)

if __name__ == "__main__":
    app.run(debug=True)

この修正版では、render_template や render_template_string に直接ユーザー入力を埋め込むのではなく、name=user_name という形でデータを分離して渡しています。これによって、テンプレートエンジンは入力値を「ただの人名などの文字列」として扱うため、不正なコードが実行されるリスクを完全にシャットアウトできます。

—

5. 防御のまとめ:一歩ずつ安全な開発を

SSTIを防ぐためのポイントを、最後にギュッとまとめておきますね。実務の現場では、次のルールをチーム全体で徹底しましょう。

1. テンプレート文字列にユーザー入力を直接結合しない
f"Hello {user_input}" のように、文字列の足し算やフォーマット関数でテンプレートを組み立てるのは絶対にやめましょう。
2. データを引き渡すときは必ず「コンテキスト(変数)」として渡す
エンジン側があらかじめ用意している「データを埋め込む仕組み」を正しく使い、データとコード(ロジック)をきっちり分離させましょう。
3. 安全なフレームワークの機能を活用する
モダンなWebフレームワークやテンプレートエンジンには、自動的に危険な文字を無害化(エスケープ)してくれる機能が備わっています。デフォルトの親切な機能を勝手にオフにしないことが大切です。

セキュリティの対策は、一度にすべてを完璧にやろうとすると息切れしてしまいます。まずは「ユーザーの入力をテンプレートのコードに直接混ぜていないかな?」と、自分の書いたコードを振り返ることから一歩ずつ始めてみましょう。

あなたのその丁寧な心がけが、安全で快適なWebの未来を作ります。一緒にエンジニアリングを楽しんでいきましょうね!

コメント

タイトルとURLをコピーしました