SsrfGuard
in package
外部 URL をサーバー側で取得する経路の SSRF ガード。
取得先ホストを名前解決し、ループバック・プライベート・リンクローカルなどの 予約済みアドレスへ向かうリクエストを拒否する。社内 API のように意図的に 内部宛の取得を許可したい場合は、許可リスト(IP アドレスまたは CIDR)で免除する。
Table of Contents
Constants
- DEFAULT_WHITE_LIST_ENV_KEY = 'ACMS_SSRF_WHITE_LIST'
- 許可リストを解決する既定の環境変数キー
- ALLOWED_SCHEMES = ['http', 'https']
- 取得を許可するスキーム
- DENIED_SUBNETS = [ '::ffff:0:0/96', // IPv4-mapped(PHP 8.1/8.2 の filter_var は弾かない) '::/96', // IPv4-compatible '2002::/16', // 6to4 '2001::/32', // Teredo '64:ff9b::/96', // NAT64 well-known prefix '64:ff9b:1::/48', ]
- filter_var() の NO_PRIV_RANGE / NO_RES_RANGE では弾けない、内部アドレスを埋め込める IPv6 の遷移表記。
Properties
- $dnsResolver : callable(string): array<string|int, string>
- ホスト名から IP アドレス一覧を解決するリゾルバ
- $whiteList : array<string|int, string>
- SSRF ガードを免除する IP アドレス/CIDR 一覧
Methods
- __construct() : mixed
- assertSafeUri() : void
- URI の取得先が内部/予約済みアドレスを指していないか検証する
- isAllowedHttpScheme() : bool
- 取得を許可するスキームかどうかを判定する
- parseWhiteList() : array<string|int, string>
- カンマ区切りの設定値を許可リストへ変換する
- resolveSafeIpAddresses() : array<string|int, string>
- URI の取得先を検証し、検証を通った IP アドレス一覧を返す。
- ipMatchesEntry() : bool
- IP アドレスが許可リストの 1 エントリ(単一 IP または CIDR 表記)に一致するか判定する
- isSafeIpAddress() : bool
- プライベート/予約済みアドレスでないか確認する
- isWhitelistedIpAddress() : bool
- IP アドレスが許可リストのいずれかのエントリに一致するか確認する
- resolveIpAddressesByDns() : array<string|int, string>
- ホスト名を解決して、検証対象の IP アドレス一覧を返す既定のリゾルバ
Constants
DEFAULT_WHITE_LIST_ENV_KEY
許可リストを解決する既定の環境変数キー
public
mixed
DEFAULT_WHITE_LIST_ENV_KEY
= 'ACMS_SSRF_WHITE_LIST'
ALLOWED_SCHEMES
取得を許可するスキーム
private
mixed
ALLOWED_SCHEMES
= ['http', 'https']
DENIED_SUBNETS
filter_var() の NO_PRIV_RANGE / NO_RES_RANGE では弾けない、内部アドレスを埋め込める IPv6 の遷移表記。
private
mixed
DENIED_SUBNETS
= [
'::ffff:0:0/96',
// IPv4-mapped(PHP 8.1/8.2 の filter_var は弾かない)
'::/96',
// IPv4-compatible
'2002::/16',
// 6to4
'2001::/32',
// Teredo
'64:ff9b::/96',
// NAT64 well-known prefix
'64:ff9b:1::/48',
]
これらは IPv4 アドレスを IPv6 表記の中に埋め込むもので、filter_var() はビット列を 予約帯と比較するだけなので埋め込まれた IPv4 を見ない。たとえば ::7f00:1 / 2002:7f00:1:: / 64:ff9b::7f00:1 はいずれも 127.0.0.1 を指すが素通りする (Symfony の NoPrivateNetworkHttpClient も同じ穴で CVE-2026-48736 が出ている)。 6to4 は RFC 7526 で廃止済み、Teredo・NAT64 も外部コンテンツの取得先として 正当な用途が無いため、帯ごと拒否する。
IPv4-mapped(::ffff:0:0/96)は PHP 8.3 以降の filter_var では弾かれるが 8.1 / 8.2 では 通過する(CI で実測)。サポート対象の全バージョンで同じ判定にするため明示的に持つ。
Properties
$dnsResolver
ホスト名から IP アドレス一覧を解決するリゾルバ
private
callable(string): array<string|int, string>
$dnsResolver
$whiteList
SSRF ガードを免除する IP アドレス/CIDR 一覧
private
array<string|int, string>
$whiteList
Methods
__construct()
public
__construct([array<string|int, string>|null $whiteList = null ][, callable(string): array<string|int, string>|null $dnsResolver = null ]) : mixed
Parameters
- $whiteList : array<string|int, string>|null = null
-
免除する IP アドレス/CIDR 一覧。null なら既定の環境変数から解決する
- $dnsResolver : callable(string): array<string|int, string>|null = null
-
名前解決の実装(テスト時の差し替え用)
assertSafeUri()
URI の取得先が内部/予約済みアドレスを指していないか検証する
public
assertSafeUri(string $uri) : void
Parameters
- $uri : string
Tags
isAllowedHttpScheme()
取得を許可するスキームかどうかを判定する
public
static isAllowedHttpScheme(string $uri) : bool
Parameters
- $uri : string
Return values
boolparseWhiteList()
カンマ区切りの設定値を許可リストへ変換する
public
static parseWhiteList(string $value) : array<string|int, string>
Parameters
- $value : string
-
カンマ区切りの IP アドレス/CIDR
Return values
array<string|int, string>resolveSafeIpAddresses()
URI の取得先を検証し、検証を通った IP アドレス一覧を返す。
public
resolveSafeIpAddresses(string $uri) : array<string|int, string>
Why not: 検証だけで IP を捨てないのは、検証と実接続で別々に名前解決すると、 その間に DNS 応答を切り替えられる(DNS リバインディングによる TOCTOU)ため。 呼び出し側は返された IP をそのまま接続先に固定(DNS ピンニング)すること。
Parameters
- $uri : string
Tags
Return values
array<string|int, string> —検証を通った IP アドレス一覧
ipMatchesEntry()
IP アドレスが許可リストの 1 エントリ(単一 IP または CIDR 表記)に一致するか判定する
private
static ipMatchesEntry(string $ipAddress, string $entry) : bool
IPv4/IPv6 の両方に対応する。
Parameters
- $ipAddress : string
- $entry : string
Return values
boolisSafeIpAddress()
プライベート/予約済みアドレスでないか確認する
private
isSafeIpAddress(string $ipAddress) : bool
Parameters
- $ipAddress : string
Return values
boolisWhitelistedIpAddress()
IP アドレスが許可リストのいずれかのエントリに一致するか確認する
private
isWhitelistedIpAddress(string $ipAddress) : bool
Parameters
- $ipAddress : string
Return values
boolresolveIpAddressesByDns()
ホスト名を解決して、検証対象の IP アドレス一覧を返す既定のリゾルバ
private
static resolveIpAddressesByDns(string $host) : array<string|int, string>
dns_get_record() と gethostbynamel() の両方を引き、結果を合併して返す。
Why not: どちらか一方を優先してもう一方をフォールバックにしないのは、両者でホストの 解釈が違い、片方だけ見ると実接続先を取り逃がすため。たとえば "0177.0.0.1" は dns_get_record が DNS 問い合わせとして扱って 177.0.0.1(公開 IP)を返す一方、 gethostbynamel は inet_aton の 8 進数解釈で 127.0.0.1 を返す。curl が実際に繋ぐのは 後者なので、dns_get_record だけを見ると内部宛の接続を許してしまう(実測で確認)。 合併して全件を検証すれば、どちらの解釈で内部アドレスになる場合も拒否できる。
両方が空のときだけ「解決不能」として拒否する。dns_get_record が無効化されている 環境でも gethostbynamel が効けば従来どおり取得できる。
Parameters
- $host : string