VYPR
High severityNVD Advisory· Published Jun 27, 2022· Updated Apr 23, 2025

CURLOPT_HTTPAUTH option not cleared on change of origin in Guzzle

CVE-2022-31090

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.

PackageAffected versionsPatched versions
guzzlehttp/guzzlePackagist
< 6.5.86.5.8
guzzlehttp/guzzlePackagist
>= 7.0.0, < 7.4.57.4.5

Affected products

2

Patches

1
1dd98b0564cb

Release 7.4.5 (#3043)

https://github.com/guzzle/guzzleGraham CampbellJun 20, 2022via ghsa
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

News mentions

0

No linked articles in our index yet.