The URI Extension

Parsing and Building URLs with the URI Extension

parse_url() follows no standard, so it can disagree with the client that fetches the URL, which defeats SSRF allow-lists. PHP 8.5 adds two immutable parsers: Uri\Rfc3986\Uri (uriparser) and Uri\WhatWg\Url (Lexbor, as in browsers).

Two standards against parse_url(), then resolving and modifying a URLPHP
<?php
use Uri\Rfc3986\Uri;
use Uri\WhatWg\Url;
foreach (['HTTPS://Shop.Example.COM:443/a/./b/../c', 'https://bücher.example/',
  'https://example.com\@evil.test/'] as $in) {
  printf("%s  rfc3986 %s\n  whatwg %s  parse_url %s\n", $in,
    Uri::parse($in)?->toString() ?? 'invalid', Url::parse($in)?->toAsciiString() ?? 'invalid',
    parse_url($in, PHP_URL_HOST));
}
echo (new Uri('https://api.example.com/v1/products/7'))->resolve('../search')
  ->withQuery(http_build_query(['q' => 'SQL & PHP', 'page' => 2]))->toString(), "\n";
Output
HTTPS://Shop.Example.COM:443/a/./b/../c  rfc3986 https://shop.example.com:443/a/c
  whatwg https://shop.example.com/a/c  parse_url Shop.Example.COM
https://bücher.example/  rfc3986 invalid
  whatwg https://xn--bcher-kva.example/  parse_url bücher.example
https://example.com\@evil.test/  rfc3986 invalid
  whatwg https://example.com/@evil.test/  parse_url evil.test
https://api.example.com/v1/search?q=SQL+%26+PHP&page=2

Only WHATWG drops default ports, converts IDN hosts to Punycode, and rejects relative input. In the third case, parse_url() says evil.test, while a browser reads \ as / and visits example.com. Check a URL and fetch it with the same parser.