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

Change in port should be considered a change in origin in Guzzle

CVE-2022-31091

Description

Guzzle, a PHP HTTP client, fails to remove sensitive Authorization and Cookie headers when following a redirect to a different port, potentially leaking credentials to untrusted third parties.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Guzzle, a PHP HTTP client, fails to remove sensitive Authorization and Cookie headers when following a redirect to a different port, potentially leaking credentials to untrusted third parties.

Vulnerability

Description

Guzzle, an extensible PHP HTTP client, contains a vulnerability where sensitive HTTP headers such as Authorization and Cookie are not removed when following a redirect to a URI with a different port. The official description notes that previously, only a change in host or scheme would trigger header removal, but a change in port was overlooked [1]. This oversight means that when a redirect response (e.g., HTTP 302) indicates a Location header with a different port, Guzzle may forward the original request's credentials to that new endpoint.

Attack

Vector and Requirements

An attacker does not need direct network access to the victim; instead, the vulnerability is exploitable through a malicious or compromised server that the client interacts with, which can issue a redirect to a different port. No special authentication is required for the attacker beyond controlling the redirect destination. The client must be configured to follow redirects (the default behavior). The partial fix in Guzzle 7.4.2 only covered changes in host, but not scheme or port, leaving the port-change attack unpatched [1]. The vulnerability affects all Guzzle versions prior to 6.5.8 and 7.4.5, as well as earlier series that are no longer supported [1][3].

Impact

If an attacker can cause the Guzzle client to follow a redirect to a different port, the client will send the original Authorization and Cookie headers to the attacker-controlled server. This can lead to credential theft, session hijacking, or unauthorized access to other services, depending on the nature of the leaked headers. The impact is especially severe for applications using HTTP Basic or Digest authentication, or relying on cookies for session management.

Mitigation

The Guzzle project released fixed versions 6.5.8 and 7.4.5 that properly remove sensitive headers on any redirect where the host, scheme, or port changes [1][2]. Users should upgrade to these versions immediately. For those unable to upgrade, an alternative is to implement custom redirect middleware that strips sensitive headers manually, or to disable following redirects entirely if not needed [1]. Note that as of 2023, Guzzle 6.x reached end-of-life, so users of that series are strongly encouraged to upgrade to 7.x for continued security support [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

7

News mentions

0

No linked articles in our index yet.