CURLOPT_HTTPAUTH option not cleared on change of origin in Guzzle
Description
Guzzle PHP HTTP client leaks Authorization header on cross-origin redirects when using Curl handler; fixed in 7.4.5 and 6.5.8.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Guzzle PHP HTTP client leaks Authorization header on cross-origin redirects when using Curl handler; fixed in 7.4.5 and 6.5.8.
Vulnerability
Description In Guzzle, an extensible PHP HTTP client, when using the Curl handler, the CURLOPT_HTTPAUTH option is used to specify an Authorization header. However, when a request results in a redirect to a URI with a different origin (change in host, scheme, or port) and redirects are followed, the CURLOPT_HTTPAUTH option is not cleared before the subsequent request. This causes curl to append the Authorization header to the redirected request, potentially leaking sensitive credentials to a third-party server [1][3].
Exploitation
An attacker can exploit this vulnerability by crafting a redirect response from a legitimate server (or by compromising it) that points to an attacker-controlled origin. If the client follows the redirect, the Authorization header (e.g., HTTP Basic or Digest credentials) is sent to the attacker's server. The attack requires that the client uses the Curl handler and follows redirects. A partial fix in Guzzle 7.4.2 only addressed host changes, not scheme or port changes [1][2].
Impact
Successful exploitation results in the disclosure of sensitive authentication credentials to an unauthorized third party. This can lead to account takeover or unauthorized access to protected resources.
Mitigation
Users should upgrade to Guzzle 7.4.5 or 6.5.8, which correctly clears the CURLOPT_HTTPAUTH option on any cross-origin redirect [1][2][4]. If immediate upgrade is not possible, disabling redirects or switching to the Guzzle stream handler backend can mitigate the issue [3].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
guzzlehttp/guzzlePackagist | < 6.5.8 | 6.5.8 |
guzzlehttp/guzzlePackagist | >= 7.0.0, < 7.4.5 | 7.4.5 |
Affected products
2Patches
11dd98b0564cbRelease 7.4.5 (#3043)
5 files changed · +137 −62
CHANGELOG.md+5 −0 modified@@ -2,6 +2,11 @@ Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version. +## 7.4.5 - 2022-06-20 + +* Fix change in port should be considered a change in origin +* Fix `CURLOPT_HTTPAUTH` option not cleared on change of origin + ## 7.4.4 - 2022-06-09 * Fix failure to strip Authorization header on HTTP downgrade
composer.json+1 −1 modified@@ -54,7 +54,7 @@ "php": "^7.2.5 || ^8.0", "ext-json": "*", "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.8.3 || ^2.1", + "guzzlehttp/psr7": "^1.9 || ^2.4", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" },
README.md+5 −5 modified@@ -44,7 +44,7 @@ We use GitHub issues only to discuss bugs and new features. For support please r - [Documentation](https://docs.guzzlephp.org) - [Stack Overflow](https://stackoverflow.com/questions/tagged/guzzle) -- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](http://slack.httplug.io/) +- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/) - [Gitter](https://gitter.im/guzzle/guzzle) @@ -73,10 +73,10 @@ composer require guzzlehttp/guzzle [guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3 [guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5 [guzzle-7-repo]: https://github.com/guzzle/guzzle -[guzzle-3-docs]: http://guzzle3.readthedocs.org -[guzzle-5-docs]: http://docs.guzzlephp.org/en/5.3/ -[guzzle-6-docs]: http://docs.guzzlephp.org/en/6.5/ -[guzzle-7-docs]: http://docs.guzzlephp.org/en/latest/ +[guzzle-3-docs]: https://guzzle3.readthedocs.io/ +[guzzle-5-docs]: https://docs.guzzlephp.org/en/5.3/ +[guzzle-6-docs]: https://docs.guzzlephp.org/en/6.5/ +[guzzle-7-docs]: https://docs.guzzlephp.org/en/latest/ ## Security
src/RedirectMiddleware.php+4 −29 modified@@ -88,10 +88,8 @@ public function checkRedirect(RequestInterface $request, array $options, Respons $this->guardMax($request, $response, $options); $nextRequest = $this->modifyRequest($request, $options, $response); - // If authorization is handled by curl, unset it if host is different. - if ($request->getUri()->getHost() !== $nextRequest->getUri()->getHost() - && defined('\CURLOPT_HTTPAUTH') - ) { + // If authorization is handled by curl, unset it if URI is cross-origin. + if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $nextRequest->getUri()) && defined('\CURLOPT_HTTPAUTH')) { unset( $options['curl'][\CURLOPT_HTTPAUTH], $options['curl'][\CURLOPT_USERPWD] @@ -198,38 +196,15 @@ public function modifyRequest(RequestInterface $request, array $options, Respons $modify['remove_headers'][] = 'Referer'; } - // Remove Authorization and Cookie headers if required. - if (self::shouldStripSensitiveHeaders($request->getUri(), $modify['uri'])) { + // Remove Authorization and Cookie headers if URI is cross-origin. + if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $modify['uri'])) { $modify['remove_headers'][] = 'Authorization'; $modify['remove_headers'][] = 'Cookie'; } return Psr7\Utils::modifyRequest($request, $modify); } - /** - * Determine if we should strip sensitive headers from the request. - * - * We return true if either of the following conditions are true: - * - * 1. the host is different; - * 2. the scheme has changed, and now is non-https. - */ - private static function shouldStripSensitiveHeaders( - UriInterface $originalUri, - UriInterface $modifiedUri - ): bool { - if (\strcasecmp($originalUri->getHost(), $modifiedUri->getHost()) !== 0) { - return true; - } - - if ($originalUri->getScheme() !== $modifiedUri->getScheme() && 'https' !== $modifiedUri->getScheme()) { - return true; - } - - return false; - } - /** * Set the appropriate URL on the request based on the location header. */
tests/RedirectMiddlewareTest.php+122 −27 modified@@ -272,65 +272,105 @@ public function testInvokesOnRedirectForRedirects() self::assertTrue($call); } - public function crossOriginRedirectProvider() + /** + * @testWith ["digest"] + * ["ntlm"] + */ + public function testRemoveCurlAuthorizationOptionsOnRedirectCrossHost($auth) { - return [ - ['http://example.com?a=b', 'http://test.com/', false], - ['https://example.com?a=b', 'https://test.com/', false], - ['http://example.com?a=b', 'https://test.com/', false], - ['https://example.com?a=b', 'http://test.com/', false], - ['http://example.com?a=b', 'http://example.com/', true], - ['https://example.com?a=b', 'https://example.com/', true], - ['http://example.com?a=b', 'https://example.com/', true], - ['https://example.com?a=b', 'http://example.com/', false], - ]; + if (!defined('\CURLOPT_HTTPAUTH')) { + self::markTestSkipped('ext-curl is required for this test'); + } + + $mock = new MockHandler([ + new Response(302, ['Location' => 'http://test.com']), + static function (RequestInterface $request, $options) { + self::assertFalse( + isset($options['curl'][\CURLOPT_HTTPAUTH]), + 'curl options still contain CURLOPT_HTTPAUTH entry' + ); + self::assertFalse( + isset($options['curl'][\CURLOPT_USERPWD]), + 'curl options still contain CURLOPT_USERPWD entry' + ); + return new Response(200); + } + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); } /** - * @dataProvider crossOriginRedirectProvider + * @testWith ["digest"] + * ["ntlm"] */ - public function testHeadersTreatmentOnRedirect($originalUri, $targetUri, $shouldBePresent) + public function testRemoveCurlAuthorizationOptionsOnRedirectCrossPort($auth) { - $mock = new MockHandler([ - new Response(302, ['Location' => $targetUri]), - static function (RequestInterface $request) use ($shouldBePresent) { - self::assertSame($shouldBePresent, $request->hasHeader('Authorization')); - self::assertSame($shouldBePresent, $request->hasHeader('Cookie')); + if (!defined('\CURLOPT_HTTPAUTH')) { + self::markTestSkipped('ext-curl is required for this test'); + } + $mock = new MockHandler([ + new Response(302, ['Location' => 'http://example.com:81/']), + static function (RequestInterface $request, $options) { + self::assertFalse( + isset($options['curl'][\CURLOPT_HTTPAUTH]), + 'curl options still contain CURLOPT_HTTPAUTH entry' + ); + self::assertFalse( + isset($options['curl'][\CURLOPT_USERPWD]), + 'curl options still contain CURLOPT_USERPWD entry' + ); return new Response(200); } ]); $handler = HandlerStack::create($mock); $client = new Client(['handler' => $handler]); - $client->get($originalUri, ['auth' => ['testuser', 'testpass'], 'headers' => ['Cookie' => 'foo=bar']]); + $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); } - public function testNotRemoveAuthorizationHeaderOnRedirect() + /** + * @testWith ["digest"] + * ["ntlm"] + */ + public function testRemoveCurlAuthorizationOptionsOnRedirectCrossScheme($auth) { + if (!defined('\CURLOPT_HTTPAUTH')) { + self::markTestSkipped('ext-curl is required for this test'); + } + $mock = new MockHandler([ - new Response(302, ['Location' => 'http://example.com/2']), - static function (RequestInterface $request) { - self::assertTrue($request->hasHeader('Authorization')); + new Response(302, ['Location' => 'http://example.com?a=b']), + static function (RequestInterface $request, $options) { + self::assertFalse( + isset($options['curl'][\CURLOPT_HTTPAUTH]), + 'curl options still contain CURLOPT_HTTPAUTH entry' + ); + self::assertFalse( + isset($options['curl'][\CURLOPT_USERPWD]), + 'curl options still contain CURLOPT_USERPWD entry' + ); return new Response(200); } ]); $handler = HandlerStack::create($mock); $client = new Client(['handler' => $handler]); - $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass']]); + $client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); } /** * @testWith ["digest"] * ["ntlm"] */ - public function testRemoveCurlAuthorizationOptionsOnRedirect($auth) + public function testRemoveCurlAuthorizationOptionsOnRedirectCrossSchemeSamePort($auth) { if (!defined('\CURLOPT_HTTPAUTH')) { self::markTestSkipped('ext-curl is required for this test'); } $mock = new MockHandler([ - new Response(302, ['Location' => 'http://test.com']), + new Response(302, ['Location' => 'http://example.com:80?a=b']), static function (RequestInterface $request, $options) { self::assertFalse( isset($options['curl'][\CURLOPT_HTTPAUTH]), @@ -345,7 +385,7 @@ static function (RequestInterface $request, $options) { ]); $handler = HandlerStack::create($mock); $client = new Client(['handler' => $handler]); - $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); + $client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); } /** @@ -377,6 +417,61 @@ static function (RequestInterface $request, $options) { $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]); } + public function crossOriginRedirectProvider() + { + return [ + ['http://example.com/123', 'http://example.com/', false], + ['http://example.com/123', 'http://example.com:80/', false], + ['http://example.com:80/123', 'http://example.com/', false], + ['http://example.com:80/123', 'http://example.com:80/', false], + ['http://example.com/123', 'https://example.com/', true], + ['http://example.com/123', 'http://www.example.com/', true], + ['http://example.com/123', 'http://example.com:81/', true], + ['http://example.com:80/123', 'http://example.com:81/', true], + ['https://example.com/123', 'https://example.com/', false], + ['https://example.com/123', 'https://example.com:443/', false], + ['https://example.com:443/123', 'https://example.com/', false], + ['https://example.com:443/123', 'https://example.com:443/', false], + ['https://example.com/123', 'http://example.com/', true], + ['https://example.com/123', 'https://www.example.com/', true], + ['https://example.com/123', 'https://example.com:444/', true], + ['https://example.com:443/123', 'https://example.com:444/', true], + ]; + } + + /** + * @dataProvider crossOriginRedirectProvider + */ + public function testHeadersTreatmentOnRedirect($originalUri, $targetUri, $isCrossOrigin) + { + $mock = new MockHandler([ + new Response(302, ['Location' => $targetUri]), + static function (RequestInterface $request) use ($isCrossOrigin) { + self::assertSame(!$isCrossOrigin, $request->hasHeader('Authorization')); + self::assertSame(!$isCrossOrigin, $request->hasHeader('Cookie')); + + return new Response(200); + } + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + $client->get($originalUri, ['auth' => ['testuser', 'testpass'], 'headers' => ['Cookie' => 'foo=bar']]); + } + + public function testNotRemoveAuthorizationHeaderOnRedirect() + { + $mock = new MockHandler([ + new Response(302, ['Location' => 'http://example.com/2']), + static function (RequestInterface $request) { + self::assertTrue($request->hasHeader('Authorization')); + return new Response(200); + } + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass']]); + } + /** * Verifies how RedirectMiddleware::modifyRequest() modifies the method and body of a request issued when * encountering a redirect response.
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-25mq-v84q-4j7rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-31090ghsaADVISORY
- security.gentoo.org/glsa/202305-24ghsavendor-advisoryWEB
- www.debian.org/security/2022/dsa-5246ghsavendor-advisoryWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/guzzlehttp/guzzle/CVE-2022-31090.yamlghsaWEB
- github.com/guzzle/guzzle/blob/6.5.8/CHANGELOG.mdghsaWEB
- github.com/guzzle/guzzle/blob/7.4.5/CHANGELOG.mdghsaWEB
- github.com/guzzle/guzzle/commit/1dd98b0564cb3f6bd16ce683cb755f94c10fbd82ghsaWEB
- github.com/guzzle/guzzle/security/advisories/GHSA-25mq-v84q-4j7rghsaWEB
News mentions
0No linked articles in our index yet.