Server-Side Request Forgery (SSRF) は OWASP Top 10 2021 で新設されたカテゴリで、クラウド環境におけるデータ窃取の主要経路である。特に AWS の IMDSv1 が絡んだインシデントは有名で、2019年の Capital One 事件では1億件超の個人情報が流出した。本記事では SSRF の仕組みと、2026年時点で実装すべき6つの防御パターンをコード付きで解説する。
SSRF とは
アプリケーションが任意のURLに対してHTTPリクエストを発行する機能 (URLプレビュー、Webhook、画像取得など) を悪用し、サーバー内部からしか到達できないエンドポイント を攻撃者が叩かせる手法である。
# 攻撃例: クラウドメタデータエンドポイントへのアクセス
http://169.254.169.254/latest/meta-data/iam/security-credentials/
成功すると、EC2 インスタンスに紐付いた IAM ロールの一時クレデンシャルが取得できてしまう。
Capital One 事件の概要
2019年、Capital One の WAF の設定不備を悪用し、攻撃者が SSRF で EC2 のメタデータから IAM クレデンシャルを取得、S3 から 1 億件の顧客データを持ち出した。事件後 AWS は IMDSv2 (セッション指向、デフォルトで Hop Limit 1) をリリースし、根本的な緩和策を提供した。
実装パターン1: 宛先URLのアローリスト化
ブラックリストではなくアローリスト方式にする。攻撃者は IPv6、0.0.0.0、10進数表記、DNSリバインディングなどあらゆる手法で回避してくる。
from urllib.parse import urlparse
ALLOWED_HOSTS = {"api.example.com", "img.example.com"}
def is_safe_url(url: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ("https",):
return False
if parsed.hostname not in ALLOWED_HOSTS:
return False
return True
実装パターン2: IPレベルでのプライベート範囲ブロック
DNS 解決後の IP アドレスを明示的に検証する。これが重要で、URLのホスト名だけ見てもDNSリバインディングで突破される。
import socket
import ipaddress
BLOCKED_RANGES = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"), # クラウドメタデータ
ipaddress.ip_network("::1/128"),
ipaddress.ip_network("fd00::/8"),
ipaddress.ip_network("fe80::/10"),
]
def resolve_and_check(hostname: str) -> str:
infos = socket.getaddrinfo(hostname, None)
for family, _, _, _, sockaddr in infos:
ip = ipaddress.ip_address(sockaddr[0])
for net in BLOCKED_RANGES:
if ip in net:
raise ValueError(f"Blocked IP range: {ip}")
return infos[0][4][0]
実装パターン3: DNSリバインディング対策
攻撃者は TTL の短い DNS を設定し、検証時は 1.2.3.4 (公開IP)、実リクエスト時は 169.254.169.254 (メタデータ) を返す。対策は、DNSを一度解決した結果の IP で直接リクエストを送ることである。
import requests
def safe_get(url: str):
parsed = urlparse(url)
ip = resolve_and_check(parsed.hostname)
# 解決済みIPで直接叩き、HostヘッダでSNIを維持
return requests.get(
f"{parsed.scheme}://{ip}{parsed.path}",
headers={"Host": parsed.hostname},
timeout=5,
verify=True
)
実装パターン4: AWS IMDSv2 の強制
EC2 のメタデータアクセスを IMDSv2 のみに制限する。IMDSv2 はセッショントークン必須のため、単純な GET で取得できなくなる。
# 既存インスタンスに強制
aws ec2 modify-instance-metadata-options \
--instance-id i-0abc123 \
--http-tokens required \
--http-put-response-hop-limit 1 \
--http-endpoint enabled
Terraform なら:
resource "aws_instance" "web" {
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
metadata_options {
http_tokens = "required"
http_put_response_hop_limit = 1
http_endpoint = "enabled"
}
}
Hop Limit 1 により、コンテナ (Docker、ECS タスク) からメタデータが取得できなくなり、SSRF経由の窃取を大幅に軽減できる。
実装パターン5: 送信専用プロキシで閉じる
外向きリクエストを必ず Squid などのプロキシ経由に限定し、プロキシ側で宛先を制御する。
# /etc/squid/squid.conf
acl allowed_hosts dstdomain api.example.com img.example.com
acl private_ips dst 10.0.0.0/8 169.254.0.0/16 127.0.0.0/8
http_access deny private_ips
http_access allow allowed_hosts
http_access deny all
アプリ側は HTTP_PROXY 環境変数でこのプロキシを使うよう強制する。
実装パターン6: Redirect の制御
HTTP 302 でリダイレクト先が内部URLになるパターンを防ぐ。各リダイレクト先も毎回検証する。
import requests
from requests.adapters import HTTPAdapter
class SafeSession(requests.Session):
def send(self, request, **kwargs):
kwargs["allow_redirects"] = False
resp = super().send(request, **kwargs)
if resp.is_redirect:
next_url = resp.headers["Location"]
if not is_safe_url(next_url):
raise ValueError("Unsafe redirect")
# 再帰的に検証しつつ最大回数を制限
return resp
実装時にハマるポイント
-
IPv6を忘れる:
::ffff:169.254.169.254のような IPv4-mapped IPv6 アドレスをブロック漏れすると突破される -
10進IP表記:
http://2852039166/は169.254.169.254と等価 -
URLスキームの想定漏れ:
file://gopher://dict://なども攻撃に使われる -
ライブラリのデフォルト挙動:
urllibrequestshttpxともにデフォルトではリダイレクト追従する
まとめ
SSRF の防御は「宛先URLのホスト名だけ検証」では不十分で、DNS解決後のIP検証、IMDSv2の強制、送信プロキシ、リダイレクト制御を多層で組み合わせる必要がある。特にクラウド環境では、アプリ側の実装に関わらず IMDSv2 Hop Limit 1 を必ず設定しておくのが最低ラインである。
参考
- OWASP SSRF Prevention Cheat Sheet
- AWS IMDSv2 ドキュメント
- PortSwigger Web Security Academy - SSRF