$attribute, 'host' => $host, 'ip' => request()->ip(), 'user_id' => auth()->id(), ]); $fail('The :attribute must not point to localhost or internal hosts.'); return; } // Block loopback (127.0.0.0/8) and link-local/metadata (169.254.0.0/16) when IP is provided directly if (filter_var($host, FILTER_VALIDATE_IP) && ($this->isLoopback($host) || $this->isLinkLocal($host))) { Log::warning('Webhook URL points to blocked IP range', [ 'attribute' => $attribute, 'host' => $host, 'ip' => request()->ip(), 'user_id' => auth()->id(), ]); $fail('The :attribute must not point to loopback or link-local addresses.'); return; } } private function isLoopback(string $ip): bool { // 127.0.0.0/8, 0.0.0.0 if ($ip === '0.0.0.0' || str_starts_with($ip, '127.')) { return true; } // IPv6 loopback $normalized = @inet_pton($ip); return $normalized !== false && $normalized === inet_pton('::1'); } private function isLinkLocal(string $ip): bool { // 169.254.0.0/16 — covers cloud metadata at 169.254.169.254 if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return false; } $long = ip2long($ip); return $long !== false && ($long >> 16) === (ip2long('169.254.0.0') >> 16); } }