Impact
When the optional referer redirect setting is enabled, affected versions of RedirectMiddleware can copy the fragment from the referring request URI into a generated Referer header. A URI fragment is the part after #. It is handled locally by the client and is not part of the HTTP request target, so the server handling the original request does not receive it. A generated Referer tells the redirect destination which URI led to the request. Guzzle correctly removes user information from that value, but retains the fragment when it follows a redirect to the same scheme, such as HTTPS to HTTPS. For example, an initial URI ending in #secret is sent without the fragment, but the redirect destination can receive a Referer ending in #secret. This behavior affects both the cURL and stream handlers.
An attacker who controls the redirect destination can read the fragment from the incoming header, including through request logs or application code. If the fragment contains a one-time login secret, access token, state value, or other private client data, it is disclosed to a server that was never meant to receive it. Exploitation requires the application to enable allowredirects.referer, make a request to a URI with a sensitive fragment, and follow a same-scheme redirect to a less-trusted destination.
The referer setting is disabled by default. Applications that leave it disabled, do not put sensitive data in URI fragments, do not follow redirects, or only redirect within the same trust boundary are not affected. Guzzle already omits the generated header when the scheme changes. This issue is limited to the fragment's inclusion. Reducing the path and query on cross-origin redirects is a separate privacy policy question.
Patches
The issue is patched in 7.15.1 and later. Starting in that release, Guzzle removes both user information and the fragment before generating a redirect Referer value. Other redirect behavior does not change. Guzzle retains the referring path and query, and continues to omit the header when the scheme changes. Versions before 7.15.1 are affected when the optional referer setting is enabled.
Workarounds
If you cannot upgrade immediately, leave automatic Referer generation disabled. It is off by default. If redirect options are configured explicitly, ensure that referer is false:
php $client->request('GET', $uri, [ 'allowredirects' => ['referer' => false], ]);
Alternatively, disable automatic redirects and follow trusted destinations manually, or remove the fragment from the request URI before sending a request that may redirect. Do not rely on the fact that fragments are absent from the initial HTTP request target, because the affected middleware can reintroduce them in the generated header.
References
https://www.rfc-editor.org/rfc/rfc9110.html#section-10.1.3 https://www.rfc-editor.org/rfc/rfc3986.html#section-3.5 https://www.rfc-editor.org/rfc/rfc9700.html#section-4.2.3
Impact
In affected versions, the built-in cURL handlers (CurlHandler and CurlMultiHandler) put every first-class request header in cURL's origin header list (CURLOPTHTTPHEADER). These handlers are the default when the PHP cURL extension is available. They move Proxy-Authorization to the proxy-only list (CURLOPTPROXYHEADER) only when Guzzle predicts an HTTP or HTTPS proxy. A "first-class" header is part of the normal request message and can be set on a PSR-7 request, through client headers defaults, the headers request option, or middleware. It does not include a literal line supplied through raw CURLOPTHTTPHEADER, CURLOPTPROXYHEADER, or streamcontext.http.header controls.
Because that migration follows Guzzle's prediction rather than the route libcurl actually takes, the credential stays in the origin list and is sent to the origin server when a request is:
- direct, including proxy set to '' to disable proxying. - bypassed by a no, noproxy, or NOPROXY match. - sent through a SOCKS proxy, which does not use the HTTP proxy header channel. - redirected from a safely proxied hop into any of those routes: redirect middleware re-evaluates the proxy per hop but, unlike Authorization and Cookie, does not strip Proxy-Authorization cross-origin.
On installations whose libcurl is older than 7.37.0, or whose PHP cURL extension lacks CURLOPTPROXYHEADER, CURLOPTHEADEROPT, and CURLHEADERSEPARATE, no proxy-only channel is available, so cURL left the header in the origin list for every route. The stream handler also serialized first-class values before selecting a proxy. PHP removes only the first Proxy-Authorization line from CONNECT, so another first-class value or a URL-userinfo Basic line could reach the tunneled origin. A later raw streamcontext.http.proxy override could instead reroute either credential directly to the origin.
The disclosed value is a private credential meant only for the proxy. RFC 9110 defines Proxy-Authorization as credentials for the next inbound proxy, and an origin is never an intended recipient. The flaw can silently give a working proxy credential to an unrelated third party. In the worst case, an attacker controls the origin and records the credential through access logs, tracing systems, or application logs. If it remains valid, the attacker can abuse a paid or access-controlled proxy, impersonate the proxy principal, or reach destinations the proxy is trusted to reach. A strong remote exploit is possible when an application sends a request to an attacker-controlled HTTP URL through a proxy with a default Proxy-Authorization header, then follows the attacker's redirect to an HTTPS or no-proxy destination that Guzzle reaches directly.
Using a first-class Proxy-Authorization header is a legitimate, documented configuration, so affected applications are not misusing the library. Guzzle does not create this field, so applications that never configure one are unaffected by the first-class-header flaw. Proxy URL userinfo is not affected on its own, but the stream handler could expose its Basic line when combined with a first-class field or a later raw streamcontext.http.proxy override. CURLOPTPROXYUSERPWD is unaffected. Literal lines supplied through raw CURLOPTHTTPHEADER, CURLOPTPROXYHEADER, or streamcontext.http.header remain caller-controlled and outside the first-class-header guarantee.
Patches
The issue is fixed in 7.14.2. The cURL handlers keep first-class Proxy-Authorization values out of the origin header list. When proxy header separation is available, they pass the values through CURLOPTPROXYHEADER with CURLHEADERSEPARATE. An empty value uses cURL's semicolon form to suppress credentials from proxy URL userinfo. On older builds, Guzzle drops the field for direct, bypassed, and SOCKS routes, but fails before network I/O if the request might use an HTTP or HTTPS proxy.
The stream handler removes the field from origin headers before choosing a route. If it selects a proxy, it accepts one value, including empty, writes a validated proxy header, and gives that value precedence over proxy URL userinfo. Multiple values, line breaks, and raw proxy overrides that could reroute generated credentials fail before connection. Direct and bypassed requests drop the field. Versions before 7.14.2 are affected by these origin-bound credential paths.
Workarounds
If you cannot upgrade immediately, remove first-class Proxy-Authorization fields from requests, client defaults, and middleware. Supply proxy credentials instead through proxy URL userinfo, for example http://user:pass@proxy.example:8080, or use CURLOPTPROXYUSERPWD with the cURL handlers. Do not combine proxy URL userinfo with a first-class field or a raw streamcontext.http.proxy override. If a first-class field is unavoidable, use libcurl 7.37.0 or newer with CURLOPTPROXYHEADER, CURLOPTHEADEROPT, and CURLHEADERSEPARATE, and ensure the field is never present on a client that can issue direct, bypassed, or SOCKS requests or follow redirects into those routes. A newer libcurl is necessary but does not fix Guzzle's route-dependent migration by itself, and disabling redirects reduces but does not eliminate exposure.
References
https://www.rfc-editor.org/rfc/rfc9110.html#section-11.7.2 https://curl.se/libcurl/c/CURLOPTPROXYHEADER.html https://curl.se/libcurl/c/CURLOPTHEADEROPT.html https://curl.se/libcurl/c/CURLOPTHTTPHEADER.html
Impact
In affected versions, CookieJar does not preserve whether a response cookie was set without a Domain attribute or with an empty one. A cookie without Domain is host-only and must be returned only to the exact host that set it. Under current cookie processing rules, an empty Domain value is also host-only. Guzzle instead stores the request host in the cookie's Domain field and later applies ordinary domain matching, as though the server had supplied a valid domain. For example, a host-only sid=secret cookie set by example.com can subsequently be sent to child.example.com. FileCookieJar and SessionCookieJar also persist the request host without recording the host-only state, so reloading a jar preserves the widened scope.
An attacker who controls or can observe a child host can therefore receive cookies that were intended only for its parent host. Depending on the cookie, this can disclose session identifiers, authorization tokens, or other sensitive state. Exploitation requires the application to enable Guzzle's cookie support, reuse the same built-in cookie jar, receive a host-only cookie from a parent host, and later make a matching request to a less-trusted child host. The cookie's other restrictions still apply. Its path must match, a Secure cookie is sent only over a secure connection, and an expired cookie is not sent.
Applications that do not use Guzzle's cookie support are not affected. Applications are also not affected by this disclosure if they use a separate jar for every host or trust boundary, never request a less-trusted subdomain with the same jar, or only store cookies carrying a valid, non-empty Domain attribute. The incorrect behavior occurs between an otherwise valid parent host and its subdomains.
Patches
The issue is patched in 7.15.1 and later. Starting in that release, Guzzle records whether a response cookie is host-only and matches it only against the exact host. The host-only flag is part of cookie identity for replacement and response-driven deletion. Cookies carrying a valid, non-empty Domain attribute retain their existing domain-matching behavior. A host-only cookie can coexist with an explicit-domain cookie having the same name, domain string, and path.
The built-in persistent jars now write a boolean HostOnly marker for every stored cookie record. They reject records where that marker is missing or is not a boolean, and validate all records before changing the live jar. Persisted cookie records written by an older version are therefore rejected rather than silently interpreted with an unsafe scope. Loading non-empty file or session data written without this marker throws a RuntimeException until the data is deleted, regenerated, or correctly annotated. Versions before 7.15.1 are affected.
Workarounds
If you cannot upgrade immediately, do not reuse one CookieJar instance across parent and child hosts with different trust levels. Use a separate cookie jar for each host or trust boundary, disable cookie handling for requests to less-trusted hosts, or avoid making those requests through a client configured with a shared jar. In particular, avoid new Client(['cookies' => true]) for a client that may contact both a trusted parent host and less-trusted subdomains, because that option creates one jar for the whole client.
When upgrading, delete or rotate existing FileCookieJar and SessionCookieJar data that contains cookie records written without a HostOnly marker. Records may instead be annotated manually only when the original presence or absence of the cookie's Domain attribute is known. The stored domain string is not sufficient to infer it safely.
References
https://www.rfc-editor.org/rfc/rfc10025.html#section-4.1.2.3 https://www.rfc-editor.org/rfc/rfc10025.html#section-5.7 https://www.rfc-editor.org/rfc/rfc10025.html#section-5.8.3
Impact
In affected versions, Guzzle's built-in CookieJar accepts any number of Set-Cookie header fields from one response, with no limit on the size of each field. When a later request matches the stored cookies, Guzzle places every match into one generated Cookie header without limiting the number of cookies or the total header length.
A malicious or compromised server can therefore return many large cookies, causing Guzzle to store attacker-controlled data in memory and copy it into later request headers. This can increase memory use and processing time. It can also make later requests fail when the generated header exceeds a limit in a handler, HTTP implementation, proxy, or destination server. A server on one sibling host, such as attacker.example.com, can also set parent-domain cookies that are later selected for another sibling, such as service.example.com. The denial can therefore affect a different service that uses the same jar.
An application is affected when it enables the built-in cookie support, receives an attacker-controlled response, and retains or reuses the jar. The issue affects both built-in handlers because Guzzle manages these cookies itself instead of using libcurl's native cookie engine. cURL addressed a similar denial-of-service issue in CVE-2022-32205 by limiting the cookies it accepts and sends, but those native limits do not protect Guzzle's separate jar. Applications that do not use cookies, use separate jars for untrusted origins, or use a third-party CookieJarInterface with suitable limits are not affected by this behavior. The demonstrated direct impact is limited to availability. The patch does not impose a lifetime limit on a jar built up over an unlimited number of responses or populated directly by application code.
Patches
The issue is patched in 7.15.1 and later. Starting in that release, the built-in CookieJar ignores a Set-Cookie field value longer than 8,190 bytes and applies at most 50 successful cookie insertions or replacements from one response. When generating a request, it emits at most 150 matching name=value pairs and limits the complete Cookie: header line to 8,190 bytes, including the field name and following space.
These limits follow the same practical shape as cURL's response to CVE-2022-32205. Both bound cookies accepted from one response, cookies added to one request, and generated header size. Guzzle's 8,190-byte incoming field limit is more generous than cURL's current 5,000-byte cookie-line limit. Neither approach adds a global jar quota or an eviction policy. The 8,190-byte incoming boundary is inclusive. Invalid, unrelated, identical, oversized, and deletion fields do not consume the 50-cookie limit. For outgoing requests, Guzzle preserves its existing matching and iteration order. It stops after 150 pairs or before the first matching cookie that would exceed the line limit. The output limits also apply to directly imported cookie state, but that state is not limited when it is added to the jar. Explicit caller-supplied Cookie headers and third-party jar implementations remain the caller's responsibility. Versions before 7.15.1 are affected.
Workarounds
If you cannot upgrade immediately, do not enable a shared built-in cookie jar for requests to untrusted origins. Use separate jars per host or trust boundary, disable cookie handling for untrusted requests, and discard or clear a jar after receiving an untrusted response before it is reused. Applications that must accept cookies from untrusted peers can provide a custom CookieJarInterface implementation that enforces suitable limits.
Guzzle does not use libcurl's cookie engine for cookies stored in a CookieJar. The cURL handler sends the Cookie header that Guzzle's cookie middleware has already built, so libcurl's cookie limits do not apply. Upgrading libcurl therefore does not fix this issue.
References
https://curl.se/docs/CVE-2022-32205.html https://www.rfc-editor.org/rfc/rfc10025.html#section-5.7 https://www.rfc-editor.org/rfc/rfc10025.html#section-5.8.3 https://www.rfc-editor.org/rfc/rfc10025.html#section-6.1