{"id":"GHSA-c2w2-prh8-qm98","summary":"guzzlehttp/psr7: Host Confusion via Weak URI Host Validation","details":"### Impact\n\n`guzzlehttp/psr7` did not reject URI host components containing authority delimiters (`/`, `?`, `#`, `@`, or `\\`), an embedded port such as `host:8080`, or IPv6 brackets that do not frame the host (such as `[::1` or `::1]`). `Uri::assertValidHost()` rejected only control characters, so these malformed hosts were accepted both when parsing a URI string — including through `new Uri()`, an absolute-form request target passed to `Message::parseRequest()`, and the synthetic `SERVER_NAME` or `SERVER_ADDR` fallback used by `ServerRequest::fromGlobals()` — and when setting a host with `Uri::withHost()`. Because the stored host was not validated, `Uri::getHost()` could return a value that does not agree with the URI's authority or with the host that is actually used on the wire. For example, `Uri::withHost('good.com@evil.com')` is accepted, and `new Uri('http://[gggg::1]/')` produces a `getHost()` of `'[gggg:'` while `getAuthority()` returns `'[gggg::1'`.\n\nYou are affected if your application builds a `Uri` or calls `Uri::withHost()` with host data that is wholly or partly attacker-controlled and then relies on `Uri::getHost()` for a security or routing decision, such as host allowlisting or denylisting, an SSRF guard, cookie scoping, or proxy-bypass selection. You are also affected if you parse, forward, proxy, or re-serialize URIs that come from untrusted raw HTTP messages or from synthetic `$_SERVER` values. Because `getHost()` can then disagree with the real connection target, the concrete impact depends on how your application and any downstream HTTP/1.x component use the URI, and may include host-based access-control bypass, server-side request forgery, routing to an unintended host, or `Host` header confusion. You are not affected if every host you place into a `Uri` is already trusted and well formed.\n\nApplications using `guzzlehttp/guzzle` are affected in the same way through Guzzle's use of PSR-7. Guzzle derives the request `Host` header from the request URI, and its cookie matching and `no_proxy` proxy-bypass matching both read `Uri::getHost()`. An application that builds a Guzzle request URI from attacker-controlled host input can therefore have its `Host` header, cookie scope, or proxy-bypass decision computed against a host that differs from the one Guzzle connects to. This is not the normal request-sending path, and an application that passes only trusted, well-formed URIs to Guzzle and never builds a request URI from attacker-controlled host input is not affected through Guzzle's standard client APIs.\n\n### Patches\n\nThe issue is patched in `2.12.3` and later. Starting in that release, `guzzlehttp/psr7` rejects URI host components that contain authority delimiters, an embedded port, or IPv6 brackets that do not frame the host, both when parsing a URI and via `Uri::withHost()`, so `Uri::getHost()` agrees with the URI authority. A framed bracketed literal whose contents are not a valid IPv6 address is preserved rather than rejected, so `getHost()` still agrees with the authority.\n\n### Workarounds\n\nIf you cannot upgrade immediately, validate and normalize host values before you build or modify a `Uri` from untrusted input, and do not treat `Uri::getHost()` as a trust boundary for a host you have not independently checked. Before passing a host to `Uri`, reject any value that contains `/`, `?`, `#`, `@`, or `\\`, that contains a colon outside a bracketed IPv6 literal, or that has unbalanced brackets. Applications that parse, forward, replay, or re-serialize raw HTTP messages, or that run with attacker-controlled `$_SERVER` values, should validate the host before calling `Message::parseRequest()` or `ServerRequest::fromGlobals()`, and should avoid reparsing untrusted input.\n\n### References\n\n* https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2\n* https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3\n* https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3","aliases":["CVE-2026-59882"],"modified":"2026-07-21T18:45:58.676523447Z","published":"2026-07-21T18:35:25Z","database_specific":{"cwe_ids":["CWE-20","CWE-436"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-07-21T18:35:25Z","nvd_published_at":"2026-07-08T17:17:26Z"},"references":[{"type":"WEB","url":"https://github.com/guzzle/psr7/security/advisories/GHSA-c2w2-prh8-qm98"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-59882"},{"type":"WEB","url":"https://github.com/guzzle/psr7/pull/811"},{"type":"WEB","url":"https://github.com/guzzle/psr7/commit/ddd64f17d4cc1f7e5ffe6fd2c989ec7221712580"},{"type":"PACKAGE","url":"https://github.com/guzzle/psr7"},{"type":"WEB","url":"https://github.com/guzzle/psr7/releases/tag/2.12.3"}],"affected":[{"package":{"name":"guzzlehttp/psr7","ecosystem":"Packagist","purl":"pkg:composer/guzzlehttp/psr7"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.12.3"}]}],"versions":["1.0.0","1.1.0","1.2.0","1.2.1","1.2.2","1.2.3","1.3.0","1.3.1","1.4.0","1.4.1","1.4.2","1.5.0","1.5.1","1.5.2","1.6.0","1.6.1","1.7.0","1.8.0","1.8.1","1.8.2","1.8.3","1.8.4","1.8.5","1.9.0","1.9.1","2.0.0","2.0.0-beta1","2.0.0-rc1","2.1.0","2.1.1","2.1.2","2.10.0","2.10.1","2.10.2","2.10.3","2.10.4","2.11.0","2.11.1","2.12.0","2.12.1","2.12.2","2.2.0","2.2.1","2.2.2","2.3.0","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4","2.4.5","2.5.0","2.5.1","2.6.0","2.6.1","2.6.2","2.6.3","2.7.0","2.7.1","2.8.0","2.8.1","2.9.0","2.9.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-c2w2-prh8-qm98/GHSA-c2w2-prh8-qm98.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N"}]}