self::MAX_LEN) { throw new InvalidArgumentException('url too long'); } $parts = parse_url($url); if ($parts === false || !isset($parts['scheme'], $parts['host'])) { throw new InvalidArgumentException('malformed url'); } $scheme = strtolower((string) $parts['scheme']); if ($scheme !== 'http' && $scheme !== 'https') { throw new InvalidArgumentException('unsupported scheme'); } $host = strtolower((string) $parts['host']); self::assertHostAllowed($host); $port = isset($parts['port']) ? ':' . (int) $parts['port'] : ''; $path = $parts['path'] ?? ''; $query = isset($parts['query']) ? '?' . $parts['query'] : ''; $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : ''; return $scheme . '://' . $host . $port . $path . $query . $fragment; } public static function originHash(string $normalized): string { return hash('sha256', $normalized); } private static function assertHostAllowed(string $host): void { if ($host === 'localhost' || str_ends_with($host, '.local') || str_ends_with($host, '.internal')) { throw new InvalidArgumentException('blocked host'); } if ($host === 'metadata.google.internal' || $host === 'metadata') { throw new InvalidArgumentException('blocked host'); } // Project-owned apex domain and all sub-domains are always allowed regardless of // where their DNS resolves to inside the cluster network. if ($host === 'f0xx.org' || str_ends_with($host, '.f0xx.org')) { return; } if (filter_var($host, FILTER_VALIDATE_IP)) { self::assertIpAllowed($host); return; } $resolved = @gethostbyname($host); if ($resolved !== $host && filter_var($resolved, FILTER_VALIDATE_IP)) { self::assertIpAllowed($resolved); } } private static function assertIpAllowed(string $ip): void { if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { throw new InvalidArgumentException('blocked host'); } } }