第2部 攻撃と防御の実践
第4章 実践Webセキュリティ — SSRF・デシリアライゼーション・JWT
入門で学んだSQLインジェクションやXSSの一段先へ。見落とされがちで、しかし致命的になりやすい高度なWeb脆弱性を、攻撃の仕組みと防御の両面から掘り下げます。
本章に登場する攻撃手法・コードは、脆弱性の理解と防御のために示すものです。必ず自分が管理する環境、または明示的に許可された対象に対してのみ試してください。無許可のシステムへの試行は、目的が学習であっても不正アクセス禁止法などに触れ得ます。技術は同じでも、対象を誤れば犯罪になります(第1章)。
🎯 この章で学ぶこと
- SSRFの仕組みと、クラウドメタデータ窃取という典型的な被害・その防御
- 安全でないデシリアライゼーションが任意コード実行につながる理由
- XXE・JWTの落とし穴・SSTIといった、見落とされがちな脆弱性
- これらに共通する本質「入力を信頼して危険な操作に渡す」という一点
4.1 入門の先へ
入門コース第11章で、SQLインジェクション、XSS、CSRFといった代表的なWeb脆弱性を学びました。これらは今も現役の脅威であり、対策(プレースホルダ、出力エスケープ、CSRFトークン)は必須の土台です。本章ではそれらを既知として扱い、より高度で、レビューや自動スキャンをすり抜けやすく、しかし決まれば被害が甚大な脆弱性に踏み込みます。
先に結論を言っておきます。これから見るSSRF・デシリアライゼーション・XXE・JWT・SSTIは、表面的にはバラバラに見えて、根っこは同じです。すべて「利用者が与えた入力を検証せず信頼し、それを危険な操作(通信・オブジェクト復元・パース・署名検証・テンプレート評価)に渡してしまう」という一点から生まれます。この共通構造を意識しながら読み進めてください。
4.2 SSRF — サーバーサイドリクエストフォージェリ
SSRF(Server-Side Request Forgery)は、攻撃者がサーバーを騙して、攻撃者の望む宛先へリクエストを送らせる攻撃です。ポイントは「攻撃者自身ではなく、サーバーが」リクエストの発信元になること。サーバーは内部ネットワークに手が届き、外部からは触れない資源にアクセスできてしまいます。
典型例は「URLを受け取って、その中身を取得する」機能です。画像のプレビュー生成、Webhook、URL短縮、外部データの取り込みなどが該当します。
意図した使い方:
POST /fetch-preview body: { "url": "https://example.com/logo.png" }
→ サーバーが example.com へGETし、画像を取得
攻撃者による悪用:
POST /fetch-preview body: { "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }
→ サーバーがクラウドのメタデータエンドポイントへGETし、
その応答(=IAMの一時認証情報)を攻撃者に返してしまう
ここで登場する 169.254.169.254 は、多くのクラウド(AWS・GCP・Azure など)で使われるメタデータエンドポイントのIPです。ここには、そのインスタンスに割り当てられた一時的なクラウド認証情報が置かれていることがあり、SSRFでこれを読み出されると、攻撃者はそのサーバーの権限を乗っ取れます。SSRFがクラウド時代に「最も危険な脆弱性の一つ」とされるのは、この認証情報窃取の破壊力ゆえです。
SSRFの厄介さは、宛先の許可リストを作ろうとしても回避手段が豊富な点にあります。127.0.0.1 を弾いても 0.0.0.0、10進数表記のIP、DNSリバインディング、リダイレクトの悪用など、抜け道が次々出てきます。したがって単一の対策に頼らず、多層で守ります。
| 対策 | 内容 |
|---|---|
| 宛先の許可リスト | 拒否リスト(ブラックリスト)ではなく、許可した宛先だけに接続する。名前解決後のIPで最終判定する |
| 内部・特殊アドレスの遮断 | プライベートIP、ループバック、リンクローカル(169.254.0.0/16)への接続を禁止 |
| メタデータの保護 | クラウド側の設定でメタデータへのアクセスを制限(例: AWSのIMDSv2でトークン必須化) |
| egress(外向き通信)制御 | アプリが外に出せる宛先をネットワーク側で絞る。不要な外向き通信を止める |
| リダイレクト追従の制限 | 取得先が別のURLへリダイレクトしても、無条件に追わない |
設計思想としては第2章のアタックサーフェス削減が効きます。「そもそも任意URL取得が本当に必要か?」を問い、必要なら宛先を限定し、egressを絞る。「サーバーがどこへでも通信できる」状態そのものを減らすのが最も確実です。
4.3 安全でないデシリアライゼーション
オブジェクトをバイト列や文字列に変換するのがシリアライズ、それを元のオブジェクトに復元するのがデシリアライズです。問題は、信頼できないデータをデシリアライズすると、復元の過程で任意のコードが実行され得ることです。復元処理が単なる「データの読み込み」ではなく「オブジェクトの再構築(=コードの実行を伴う)」だからです。
Pythonの pickle はその典型で、公式ドキュメントも「信頼できないデータを絶対に unpickle するな」と明記しています。仕組みを、意図的に無害な形で示します。
import pickle
import os
# 攻撃者が作る「悪意あるオブジェクト」
# __reduce__ は unpickle 時に「何をどう復元するか」を返す特殊メソッド。
# ここに危険な呼び出しを仕込める。
class Malicious:
def __reduce__(self):
# 復元時に os.system("...") を実行させる、という指示を返す
return (os.system, ("echo 攻撃者のコードが実行された",))
# 攻撃者はこれをシリアライズして、正規データを装って送りつける
payload = pickle.dumps(Malicious())
# --- サーバー側 ---
# 信頼できないデータをそのまま復元すると…
pickle.loads(payload) # ← この瞬間に os.system(...) が走る = 任意コード実行(RCE)
ここで攻撃者が仕込んでいるのは __reduce__ が返す「復元手順」です。pickle.loads はデータを読むだけのつもりでも、実際にはその手順に従ってコードを実行してしまいます。os.system の中身を悪意あるコマンドに変えれば、サーバー上で任意のコマンドが動くリモートコード実行(RCE)になります。この問題はPythonの pickle に限らず、Javaのネイティブシリアライズ、Rubyの Marshal、PHPの unserialize など、多くの言語に同種の危険があります。
(1)信頼できないデータを、コード実行を伴う形式でデシリアライズしない。外部から来るデータに pickle / Marshal / ネイティブシリアライズを使わない。(2)データ交換には JSON など「データしか表現できない」安全な形式を使う。JSONはオブジェクトの復元(コード実行)を伴わないため、この種の攻撃の土台がありません。(3)どうしてもオブジェクトを受け渡すなら、署名で完全性を保証し、許可する型を厳格に制限します。
4.4 XXE — XML外部実体注入
XXE(XML External Entity injection)は、XMLの「外部実体(external entity)」という古い機能を悪用する攻撃です。XMLでは、DTD(文書型定義)の中で「実体」を定義し、本文から参照できます。この実体の参照先にローカルファイルや内部URLを指定できてしまうのが問題の核心です。
攻撃ペイロードの例を示します。記号はすべてエスケープして表示しています。
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>
→ 外部実体 xxe が /etc/passwd を指す。パーサが実体を展開すると、
本来データが入るはずの場所にサーバー上のファイル内容が読み込まれ、
応答などを通じて攻撃者に漏れる。
XXEでできてしまうことは、サーバー上の任意ファイルの読み取り(設定ファイル、認証情報)、内部URLへのアクセス(SSRFの踏み台)、実体を再帰的に展開させるサービス妨害(いわゆる「10億の笑い」攻撃)など多岐にわたります。<!ENTITY xxe SYSTEM "http://169.254.169.254/..."> のように書けば、4.2で見たメタデータ窃取にもつながります。
対策は明快です。XMLパーサで外部実体とDTDの処理を無効化すること。多くの言語のパーサには外部実体を無効にする設定があり、これを明示的にオフにします。そもそもXMLである必然性がなければ、JSONなど外部実体の概念を持たない形式に切り替えるのが最も安全です。
4.5 JWTの落とし穴
第3章で、JWTの構造(header.payload.signature)と、署名は改ざん検知のためにあることを学びました。ところが、この署名検証をめぐって実装ミスにつけ込む攻撃が繰り返し発生しています。代表的な3つを押さえます。
alg=none 攻撃
JWTのヘッダーには署名アルゴリズムを示す alg があります。仕様には「署名なし」を意味する alg: "none" が存在し、これを受け入れてしまうライブラリ・実装があると、攻撃者は署名を空にしてpayloadを好きに書き換えられます。"role": "user" を "role": "admin" に変え、署名部を空にして送るだけで管理者になりすませてしまいます。
攻撃前: header={"alg":"RS256"} payload={"sub":"u1","role":"user"} .署名あり
攻撃後: header={"alg":"none"} payload={"sub":"u1","role":"admin"} .(署名は空)
→ サーバーが alg=none を許すと、署名検証がスキップされ改ざんが通る
署名検証そのものの欠落 / 鍵取り違え
「トークンをデコードして中身を読む」処理はしても「署名を検証する」処理を書き忘れる、というミスがあります。デコード(decode)と検証(verify)は別物です。また、非対称鍵(RS256)を想定した検証コードに、攻撃者が alg を HS256(共通鍵)に変えて送り、公開鍵を共通鍵として署名を偽造する「アルゴリズム混同」攻撃も知られています。
弱い秘密鍵
HS256のような共通鍵方式で、秘密鍵が短い・推測しやすい・辞書に載っているような値だと、攻撃者にオフラインで総当たりされ、正規の署名を作られてしまいます。
| 落とし穴 | 対策 |
|---|---|
| alg=none / アルゴリズム混同 | 受け入れるアルゴリズムをサーバー側で1つに固定し、トークンのalgを信用しない |
| 署名検証の欠落 | 「デコード」ではなく必ず「検証つきデコード」を使い、iss/aud/expも検証する |
| 弱い秘密鍵 | 十分に長くランダムな鍵を使う。鍵はコードに書かず安全に管理(第5章) |
4.6 SSTI — サーバーサイドテンプレートインジェクション
最後にSSTI(Server-Side Template Injection)を簡潔に。多くのWebアプリは、テンプレートエンジン(Jinja2、Twig、Freemarker など)でHTMLを組み立てます。SSTIは、利用者の入力をテンプレートの「データ」ではなく「テンプレートそのもの」として評価させてしまう脆弱性です。
典型的な原因は、ユーザー入力を文字列連結でテンプレート本体に埋め込むことです。テンプレートエンジンは式を評価する力を持つため、入力に埋め込まれた式が実行され、内部データの露出、さらには言語機能をたどって任意コード実行(RCE)に至ることがあります。テスト用の探り(例: {{7*7}} が 49 と表示されるか)で発見されることで知られます。
対策は、ユーザー入力をテンプレート本体に混ぜないこと。入力はあくまで「値(コンテキスト変数)」として渡し、テンプレート構造は固定します。これはXSS対策(データとコードの分離)や、SQLインジェクション対策(プレースホルダ)と全く同じ発想です。
本章の5つは、攻撃の名前も対象も違いますが、根っこは完全に同じです。SSRFは入力を「通信先」として、デシリアライゼーションは入力を「復元すべきオブジェクト」として、XXEは入力を「解釈すべきXML」として、JWTの落とし穴は入力(トークン)を「検証せず信じる情報」として、SSTIは入力を「評価すべきテンプレート」として——いずれも信頼できない入力を、検証しないまま危険な操作に引き渡しているのです。これは入門コース第10章で学んだ「入力を信頼しない」というセキュアコーディングの大原則の、応用形にほかなりません。新しい脆弱性名に出会ったら、まず問うべきは「どの入力が、検証されずに、どんな危険な操作へ流れているか」。この一問を持てば、名前を知らない攻撃にも立ち向かえます。
まとめ
- SSRFはサーバーを騙して任意の宛先へ通信させる。169.254.169.254からのクラウド認証情報窃取が典型で、許可リスト・メタデータ保護・egress制御で守る
- 安全でないデシリアライゼーションは復元時のコード実行(RCE)を招く。信頼できないデータをpickle等で復元せず、JSONなど安全な形式を使う
- XXEはXML外部実体でファイル読み取りやSSRFを引き起こす。パーサで外部実体を無効化する
- JWTはalg=none・署名検証の欠落・弱い鍵が三大落とし穴。アルゴリズムを固定し、必ず検証し、強い鍵を安全に管理する
- 共通の本質は「検証しない入力を危険な操作に渡す」こと。入門10章の原則の応用で立ち向かえる
練習問題
あるサービスに「指定したURLの画像を取り込んでサムネイルを作る」機能があります。開発者は「127.0.0.1 と localhost を拒否リストに入れたので安全です」と言っています。この対策はなぜ不十分で、どう強化すべきでしょうか。SSRFの観点で答えてください。
解答を見る
不十分な理由:SSRFは拒否リストを回避する手段が非常に多いためです。127.0.0.1 を弾いても、0.0.0.0、IPの10進数・16進数表記、[::1] などのIPv6、DNSリバインディング(名前解決の結果を後で内部IPに差し替える)、外部URLからの内部URLへのリダイレクトなどで容易にすり抜けられます。さらに、この機能で本当に危険なのはクラウドのメタデータエンドポイント 169.254.169.254(リンクローカル)であり、拒否リストに漏れがちです。強化策:拒否ではなく許可リスト方式にし、名前解決後の最終的なIPで判定する。プライベート/ループバック/リンクローカルへの接続を一律遮断する。クラウド側でメタデータを保護(IMDSv2など)する。egress制御で外向き通信の宛先を絞る。リダイレクトを無条件に追従しない。
あるAPIが、クライアントから送られてくる「設定オブジェクト」を pickle.loads() で受け取って復元しています。ここにどんな危険があり、どう設計を変えるべきか説明してください。
解答を見る
危険:クライアントは信頼できない入力源です。pickle.loads() は復元の過程でオブジェクトの再構築(コード実行を伴い得る)を行うため、攻撃者が __reduce__ などを仕込んだ悪意あるデータを送れば、サーバー上で任意コード実行(RCE)が起きます。これは「安全でないデシリアライゼーション」の典型で、最も深刻な部類の脆弱性です。設計変更:外部入力に pickle を使うのをやめ、JSONなど「データしか表現できない」安全な形式で受け取り、必要な型に自前でマッピング(バリデーションを伴う)します。JSONはオブジェクト復元(コード実行)を伴わないため、この攻撃の土台がなくなります。
JWTを使うシステムで、攻撃者がトークンのヘッダーを {"alg":"none"} に書き換え、payloadの "role" を "admin" にして署名部を空にした偽トークンを送ってきました。これが通ってしまう実装のミスは何で、どう防ぎますか。
解答を見る
これはalg=none 攻撃です。通ってしまう原因は、サーバーがトークン内のalgをそのまま信用して署名検証の方式を決めており、none(署名なし)を受け入れて検証をスキップしてしまっていることです。攻撃者が指定したアルゴリズムを信じてはいけません。防御:サーバー側で受け入れるアルゴリズムを1つ(例: RS256)に固定し、トークンのalgがそれと一致しなければ拒否する。none は決して許可しない。加えて、単なるデコードではなく署名検証つきのデコードを使い、iss(発行者)・aud(想定利用先)・exp(有効期限)も検証します。鍵は十分に長くランダムなものを安全に管理します(第5章)。