ゆめさくセキュリティ

レッスン 2-5

サーバーを踏み台にした内部への到達

外の URL を取りに行く機能は、中への入口になります。

利用者の渡した URL をサーバーが取りに行く機能では、サーバーの立ち位置から、外からは届かない内側の住所に届いてしまいます。行き先の許可、名前を引いたあとの住所の確かめ、転送の追いかけの 3 か所を、脆いコードと直したコードで比べ、IP の許可リストだけでは足りない理由までを扱います。

  • 目安60 分
  • 出典を確かめた日2026年10月24日
  • 修了の条件IP の許可リストだけでは防げない理由を説明できる

なぜ通るのか

画像の URL を貼ると、サーバーがその画像を取りに行き、縮めて表示する機能を作ったとします。プロフィールの画像、リンクの見本の表示、外のサービスからの通知の受け口 (Webhook) など、URL を受け取ってサーバーが取りに行く機能はよくあります。

ここで、サーバーが渡された URL をそのまま取りに行くと、穴が開きます。要求を送るのは利用者のブラウザではなく、サーバー自身だからです。

サーバーサイドリクエストフォージェリ (SSRF) は、この立ち位置を使って、サーバーに内側の住所へ要求を送らせる穴です。

外と内が混ざる所

サーバーは、外からは届かない場所に手が届きます。同じ機械の上の管理用の口 (127.0.0.1)、社内の網の住所、クラウドの設定の置き場などです。外の利用者が直接つないでも門で止まる場所に、サーバーの中からなら届きます。

利用者の URL をそのまま取りに行く機能は、「外の誰か」の頼みで「内側の立ち位置」から要求を送る機能になります。境目にあるはずの門を、サーバーが内側から開けてしまうわけです。

クラウドの設定の置き場

クラウドの仮想の機械には、その機械の設定を返す置き場 (メタデータのサービス、AWS では IMDS) があります。AWS の文書によると、EC2 ではこの置き場を、動いている機械の中から 169.254.169.254 の住所で引けます。AWS のセキュリティの認証の情報 (credentials) も、ここから取れると書かれています。OWASP の Cheat Sheet も、AWS、Google Cloud、Azure の置き場がこの住所にあるとして、止めるべき住所の表に載せています。

機械の中から引ける作りなので、サーバーに SSRF があると、ここに届くおそれがあります。AWS は、版 2 (IMDSv2) として、先に PUT の要求でセッションのトークンを取り、そのトークンを付けた要求だけに答える作りを用意しています。

IP の許可リストだけでは足りない

「内側の住所なら断る」という確かめを、URL の文字だけで行うと足りません。URL に書かれているのは名前で、つなぐ先の住所は名前を引いて (DNS で解決して) 初めて決まるからです。さらに、取りに行った先が「別の住所へ行け」と転送を返すと、その先は確かめを通っていません。

確かめは、名前、引いた住所、転送の先の 3 か所で行う必要があります。

脆いコードと直したコード

ラボの中の小さな関数で比べます。内側のサービスは 127.0.0.1:8081 で、外の公開サーバーの代わりは 127.0.0.1:8080 で、どちらも自分の PC の中だけで立てます。Python の標準の部品だけで動き、外とは通信しません。

行き先を確かめるか

脆いコード渡された URL をそのまま取る
from urllib.request import urlopen def fetch(url):with urlopen(url, timeout=5) as r:return r.read()
直したコード方式と名前を許可から選ぶ
from urllib.parse import urlsplit ALLOW = {"img.example.com"} def check(url):u = urlsplit(url)if u.scheme != "https":raise ValueError("scheme")if u.hostname not in ALLOW:raise ValueError("host")return u.hostname

脆い方に、内側のサービスの URL (方式は http、住所は 127.0.0.1:8081、道は /admin) を渡すと、内側のサービスの答えがそのまま返ります。直した方は、この URL を方式の段 (scheme) で断ります。方式を https に変えても、今度は名前が許可に無いので host の段で断ります。

利用者が取りに行ける先を、使う予定の名前だけに絞るのが、OWASP の勧める許可リスト (許した物だけを通す一覧) です。許可リストを持てない機能 (どこへでも通知を送る Webhook など) では、内側の住所を断る確かめと、網の側の制限を重ねます。

名前を引いたあとの住所を確かめるか

脆いコード名前だけを見る
def allowed(host):return host in ALLOW
直したコード引いた住所も確かめる
import socketfrom ipaddress import ip_address def public_ips(host):infos = socket.getaddrinfo(host, 443)ips = sorted({i[4][0] for i in infos})for ip in ips:if not ip_address(ip).is_global:raise ValueError("inside " + ip)return ips

許可した名前が、いつも外の住所を指すとは限りません。名前の持ち主が記録を書き換えれば、許可した名前でも内側の住所を指せます。ラボでは、localhost を許可に入れて試します。脆い方は名前だけを見て通します。直した方は名前を引き、127.0.0.1 が外の住所ではない (is_global が偽) ので断ります。

ただし、確かめた時と、つなぐ時で、名前を引き直すと、答えが変わることがあります。OWASP はこれを DNS の固定 (DNS pinning) の問題として挙げています。確かめた住所に固定してつなぐか、外へ出る要求を 1 か所の中継 (プロキシ) に集め、そこで行き先を絞ります。

転送を追いかけるか

脆いコード転送を自動で追う
from urllib.request import urlopen def get(url):with urlopen(url, timeout=5) as r:return r.read()
直したコード転送を追わない
from urllib.request import (build_opener, HTTPRedirectHandler) class NoFollow(HTTPRedirectHandler):def redirect_request(self, *a):return None OPENER = build_opener(NoFollow) def get(url):with OPENER.open(url, timeout=5) as r:return r.read()

ラボの 127.0.0.1:8080 は、どの要求にも「127.0.0.1:8081/admin へ行け」という転送 (302) を返すよう立てておきます。脆い方は転送を自動で追い、内側の答えを持ち帰ります。最初の URL は確かめを通っていても、転送の先は通っていません。直した方は転送を追わず、302 のエラーで止まります。

OWASP も、入力の確かめを転送で迂回されないよう、取りに行く道具の転送の追いかけを切るよう書いています。転送を追う必要がある機能なら、追うたびに、上の 2 つの確かめをやり直します。

ログでの見え方と検知

SSRF の要求は、サーバー自身から出ていくので、入ってくる要求の記録だけでは見えません。外へ出る要求を、行き先ごとに記録します。

記録直したコードが断ったときの記録
fetch refused host=localhostip=127.0.0.1 reason=insideuser=1042 feature=avatar

この行は、利用者の渡した名前が、内側の住所を指していたことを示します。守る側は、次の 3 つを記録と見張りに入れます。

見る物なぜ効くか
断った要求の数と理由ふだんの利用では、内側を指す URL はほとんど来ない
引いた住所と、つないだ住所2 つがずれたら、引き直しで答えが変わった印
転送の応答 (300 番台)取りに行く機能で転送が増えたら、迂回の疑い

クラウドでは、メタデータの置き場への要求も見張ります。AWS は、IMDSv2 で、トークンを取る PUT の要求に X-Forwarded-For ヘッダー (中継を通ったことを示すヘッダー) が付いていると断ること、PUT の応答は既定で 1 回しか中継を越えないこと (hop limit が 1) を書いています。中継の多い道からは、トークンを取りにくくする作りです。

直し方と、直ったことの確かめ方

自分の PC の中だけで行います。外のサービスや、他人の機械には向けません。待ち受けは 127.0.0.1 に固定します。

  1. 作業用のフォルダで、内側のサービスを 127.0.0.1:8081 に立てる。127.0.0.1:8080 には、8081 への転送を返すサーバーを立てる
  2. 脆い fetch に 8081 の URL を、脆い get に 8080 の URL を渡し、どちらも内側の答えを持ち帰ることを確かめる
  3. 脆い allowed に localhost を渡し、通ってしまうことを確かめる
  4. 関数を直したコードに置き換え、同じ入力が scheme、inside、302 で止まることを確かめる
  5. 下の試験を足し、直す前のコードに戻すと試験が落ちることも確かめる
  6. 終わったら、2 つのサーバーを止め、作業用のフォルダごと消す
Python直ったことを固める試験
import pytest def test_inner_url_refused():with pytest.raises(ValueError):check("http://127.0.0.1:8081/") def test_name_to_inside_refused():with pytest.raises(ValueError):public_ips("localhost")

試験は「同じ入力で、直す前は内側に届き、直したあとは断られる」ことを固定します。転送の試験は、ラボのサーバーを立てたうえで get が 302 のエラーで止まることを見ます。

あわせて、取りに行く機能を動かすサーバーから、内側への道を網の側でも絞ります。コードの確かめをすり抜けても、網で届かなければ被害は止まります。AWS の機械なら、OWASP の勧めのとおり IMDSv2 へ移し、古い版 (IMDSv1) を止めます。

関わる事例

CISA は 2024年2月29日の注意喚起 (AA24-060B) で、Ivanti Connect Secure と Ivanti Policy Secure の複数の脆弱性が悪用されていると知らせています。そのうちの 1 つ CVE-2024-21893 は、SAML (別のサービスでのログインを受け渡す仕組み) の部品にある SSRF で、攻め手が認証なしで制限された資源に届けるとしています。CISA は、ほかの脆弱性と連ねて使われうることも書いています。

JPCERT/CC も、同じ製品の脆弱性の注意喚起の中で、2024年1月31日 (現地時間) に Ivanti が SSRF の脆弱性 (CVE-2024-21893) を含めて直すパッチを出したと伝えています。VPN の機器のように境目に立つ製品では、サーバーの立ち位置から届く先が広いので、SSRF の重みが増します。

確認クイズ

1 問目

画像を取りに行く機能で、URL の名前が許可リストに入っているかを確かめています。それでも内側に届くのはどんなときですか。

  • ア 許可した名前が、内側の住所を指すように書き換えられたとき
  • イ URL が https で始まるとき
  • ウ 画像の大きさが大きいとき
答えと理由を見る

答え ア

URL に書かれているのは名前で、つなぐ先は名前を引いて決まります。名前だけを確かめても、引いた住所が内側なら届きます。引いた住所を確かめ、その住所に固定してつなぎます。

2 問目

URL も、引いた住所も確かめました。取りに行った先が、内側の住所への転送を返しました。取りに行く道具は転送を自動で追います。どうなりますか。

  • ア 最初の URL を確かめたので、安全
  • イ 転送の先は確かめを通っていないので、内側に届く
  • ウ 転送は、ブラウザだけが追う
答えと理由を見る

答え イ

確かめたのは最初の URL だけです。転送を自動で追うと、2 回目の要求は確かめを通らずに送られます。転送を追わないか、追うたびに確かめをやり直します。

説明課題

次のことを、自分の言葉で 3 行にまとめてください。書けたら、このレッスンは修了です。

  • IP の許可リストだけでは SSRF を防げない理由
模範解答を見る

URL に書かれた名前や住所を確かめても、つなぐ先は名前を引いた時に決まり、確かめた時とつなぐ時で答えが変わることがある。取りに行った先が転送を返せば、その先は確かめを通らない。だから引いた住所に固定してつなぎ、転送を追わず、網の側でも内側への道を絞る。

一次資料

どれも 2026年10月24日に開いて、題と中身を確かめました。

次へ

次のレッスンは「ファイルの受け取りとパス」です。利用者の渡す名前が、サーバーの中のどこを指すかを確かめる作りを、ファイルの保存と配信で扱います。

次のレッスン 2-6 ファイルの受け取りとパス

このレッスンにつながる物