resolveHost($host); if ($ips === []) { $fail(trans('validation.external_url')); return; } foreach ($ips as $ip) { if (! PublicIpCheck::isPublic($ip)) { $fail(trans('validation.external_url')); return; } } } private function resolveHost(string $host): array { $ips = []; // gethostbynamel goes through nsswitch (/etc/hosts, mDNS, DNS) — // the same lookup path the outbound HTTP client will use. dns_get_record // alone is a pure DNS query that ignores /etc/hosts and may apply // search suffixes, which would let "localhost" resolve to a public IP // under "localhost." and slip past the check. $v4 = @gethostbynamel($host); if (is_array($v4)) { foreach ($v4 as $ip) { $ips[] = $ip; } } // There is no stdlib nsswitch equivalent for IPv6, so this leg is // best-effort DNS. The IPv4 leg above already catches the common // "localhost" / hosts-file cases, so a missed AAAA record here // can't silently pass a name we would have otherwise rejected. $v6 = @dns_get_record($host, DNS_AAAA); if (is_array($v6)) { foreach ($v6 as $r) { if (! empty($r['ipv6'])) { $ips[] = $r['ipv6']; } } } return array_values(array_unique($ips)); } }