こんにちは!Webアプリケーションの開発やセキュリティに触れ始めたばかりの皆さん、日々のコーディングやお仕事お疲れ様です。
突然ですが、皆さんはご自宅の「鍵」や「郵便受け」の管理について、普段どう意識されていますか?「うちはオートロックだから安心」「鍵を閉めておけば泥棒は入れない」と思っていませんか?
実は、どれだけ頑丈な玄関の鍵をつけていても、「郵便受けの隙間から細い棒を入れて、内側から鍵を開けられてしまった」としたらどうでしょう。玄関の鍵自体は壊されていないのに、泥棒は簡単に家の中に入ってきてしまいますよね。
Webアプリケーションの世界でも、これと全く同じ現象が起きることがあります。それが今回お話しする SSTI(Server-Side Template Injection:サーバーサイド・テンプレート・インジェクション) という、ちょっと難しそうな名前の脆弱性です。
今回は、このSSTIがなぜ恐ろしいのか、どうやって攻撃されてしまうのかを、身近な例えを交えながら一歩ずつ優しく紐解いていきたいと思います!
—
1. テンプレートエンジンってなに?(便利な「手紙の代筆屋さん」)
まず、SSTIの主役である「テンプレートエンジン」について知る必要があります。
Webサイトを作るとき、ユーザーごとに「〇〇さん、こんにちは!」と名前を変えたり、商品の価格一覧を綺麗に表にして表示したりしたいですよね。毎回それを全部手書きでHTMLに書き直すのは大変です。
そこで登場するのが、テンプレートエンジンという仕組みです。
これは例えるなら、「あらかじめ挨拶文の枠組みだけ作っておいて、宛名の部分だけパパッと書き換えてくれる便利な『手紙の代筆屋さん』」のようなものです。
例えば、Pythonの有名なテンプレートエンジンである Jinja2 では、以下のようなテンプレートを用意します。
<!-- 挨拶文のテンプレート -->
<h1>こんにちは、{{ username }}さん!</h1>
<p>今日も良い一日を過ごしましょう。</p>
この {{ username }} の部分に、代筆屋さんが「山田」さんというデータを当てはめて、最終的にブラウザへ綺麗なHTMLを届けてくれます。便利ですよね!
—
2. 郵便受けの隙間:SSTIはどうして起きる?
さて、この便利な代筆屋さんですが、もし「おまかせで何でも書いていいよ!」と、ユーザーからの入力をそのまま丸投げしてしまったらどうなるでしょうか?
先ほどの例えに戻りましょう。代筆屋さんに「手紙の本文だけでなく、『我が家の合鍵の場所』も一緒に書いておいて!」と、悪意ある人が手紙の隙間からこっそり指示書(コマンド)を差し入れたとします。
代筆屋さんは真面目なので、指示された通りにそれを実行してしまいます。結果として、サーバーの裏側でこっそり隠された機密情報を見られてしまったり、最悪の場合はサーバーそのものを乗っ取られたりするのです。
これが、SSTIの正体です。ユーザーが入力した文字列が、ただの「文字データ」として扱われず、テンプレートエンジンの「プログラム(命令)」として誤って実行されてしまうことで発生します。
—
3. 攻撃のメカニズム:テンプレートからサーバーの奥深くへ
実際に、攻撃者がどのようにしてシステムを乗っ取ろうとするのか、そのメカニズムを少しだけ覗いてみましょう(もちろん、安全なテスト環境でのみ試すべき内容です!)。
例えば、ユーザーからの入力がそのままテンプレートに埋め込まれてしまう、次のような脆弱なコードがあったとします。
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route("/")
def home():
# ユーザーからの入力を受け取る(例: ?name=山田)
user_input = request.args.get("name", "ゲスト")
# 【危険な実装】ユーザーからの入力をそのままテンプレートとして処理している
template = f"<h1>こんにちは、{user_input}さん!</h1>"
return render_template_string(template)
ここに普通の文字(例: 山田)が入る分には問題ありません。しかし、もし攻撃者が以下のような「マジックワード(特殊な構文)」をURLのパラメータに仕込んだらどうなるでしょうか?
?name={{ 7 * 7 }}
もしここに脆弱性があれば、画面には「こんにちは、49さん!」と表示されてしまいます。文字ではなく、計算式が裏で実行されてしまったのです!
サンドボックスの壁を破る
「計算ができるだけなら怖くないのでは?」と思うかもしれませんが、ここからが攻撃者の恐ろしいところです。
テンプレートエンジンには、安全のために「これ以上は危ないから実行しちゃダメ!」というルール(サンドボックス=砂場の中だけで遊んでね、という仕組み)が用意されています。しかし、優秀な(そして悪意を持った)ハッカーたちは、その砂場のルールブックの隙間を縫って、外の世界(サーバーのOS機能)にアクセスする方法を見つけ出します。
例えば、Pythonの Jinja2 では、以下のような複雑な構文(ペイロード)を使うことで、サーバー内の環境変数(パスワードやAPIキーなどが書かれた重要なファイル)を覗き見たり、OSのコマンドを実行してサーバーを乗っ取ったりすることができてしまいます。
{# テンプレートのオブジェクトを辿って、OSの機能にアクセスしようとする悪意あるコードの例 #}
{{
self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()
}}
なんだか呪文のようですね。これは、家の鍵の構造の隙間を突いて、針金で内側からガチャリと鍵を開けてしまうようなものなのです。
—
4. 一歩ずつ学ぶ!SSTIからアプリケーションを守る対策
「なんだか恐ろしい話だな……私のサイトは大丈夫かな?」と不安になってしまったかもしれませんが、安心してください!正しい対策を知っていれば、この脆弱性は確実に防ぐことができます。
防犯と同じで、基本の対策をしっかり行うことが何よりも大切です。
対策1:ユーザーの入力を「テンプレートのコード」として解釈させない
一番根本的な解決策は、「ユーザーからの入力を、テンプレートエンジンの命令として絶対に実行しないこと」です。
多くのテンプレートエンジンでは、データを渡す際に、コードとして埋め込むのではなく、単なる「変数(データ)」として安全に渡す仕組み(コンテキストの分離)が用意されています。
【危ない書き方】
# ユーザーの入力を文字列として直接結合している(絶対にNG!)
template = "<h1>こんにちは、" + user_input + "さん!</h1>"
render_template_string(template)
【安全な書き方】
# テンプレートの枠組みは固定し、変数は安全に引き渡す
#これなら、ユーザーが {{ 7*7 }} と入力しても、ただの文字「{{ 7*7 }}」として画面に表示されます!
return render_template_string("<h1>こんにちは、{{ name }}さん!</h1>", name=user_input)
対策2:安全なフレームワークやデフォルト設定を活用する
現代のモダンなWebフレームワーク(Django, Rails, React, Vue.jsなど)の多くは、画面に文字を表示する際に自動的に「エスケープ処理(危ない記号を安全な文字に置き換える処理)」を行ってくれます。フレームワークの標準機能を信じ、独自に変数をテンプレートとして解釈させるような複雑な実装を避けることが、最大の防御になります。
—
5. まとめ
今回は、SSTI(サーバーサイド・テンプレート・インジェクション)という脆弱性について、代筆屋さんと郵便受けの例えを交えながら解説しました。
- テンプレートエンジンは便利だけど、ユーザーの入力をそのまま「命令」として実行してしまうと思わぬ隙(SSTI)が生まれる。
- 隙間を突かれると、計算だけでなく、サーバーの内部情報やOSコマンドまで実行されてしまう危険がある。
- 防ぐためには、ユーザーの入力をテンプレートのコードとして処理させず、安全な「変数」として扱うことが何よりも重要。
セキュリティの世界は一見すると難しく感じる用語が多いですが、「何がどこにつながっていて、どこに隙間があるのか」を身近なものに置き換えて考えると、本質がすっきりと見えてきます。
一歩ずつ、安全で堅牢なアプリケーション作りのスキルを一緒に磨いていきましょう!次回の解説もお楽しみに!
コメント