CVE-2022-31090: Improper Removal of Sensitive Information Before Storage or Transfer (CWE-212)
value: Guzzle, an extensible PHP HTTP client. `Authorization` headers on requests are sensitive information. In affected versions when using our Curl handler, it is possible to use the `CURLOPT_HTTPAUTH` option to specify an `Authorization` header. On making a request which responds with a redirect to a URI with a different origin (change in host, scheme or port), if we choose to follow it, we should remove the `CURLOPT_HTTPAUTH` option before continuing, stopping curl from appending the `Authorization` header to the new request. Affected Guzzle 7 users should upgrade to Guzzle 7.4.5 as soon as possible. Affected users using any earlier series of Guzzle should upgrade to Guzzle 6.5.8 or 7.4.5. Note that a partial fix was implemented in Guzzle 7.4.2, where a change in host would trigger removal of the curl-added Authorization header, however this earlier fix did not cover change in scheme or change in port. If you do not require or expect redirects to be followed, one should simply disable redirects all together. Alternatively, one can specify to use the Guzzle steam handler backend, rather than curl.
CWE description: The product stores, transfers, or shares a resource that contains sensitive information, but it does not properly remove that information before the product makes the resource available to unauthorized actors.
CVSS v3 base score: 7.7
Published: 2022-06-27T22:15Z
Repository: https://github.com/guzzle/guzzle
Language: PHP
Files changed: CHANGELOG.md, README.md, composer.json, src/RedirectMiddleware.php, tests/RedirectMiddlewareTest.php
Commit date: 2022-06-20 23:16:13 +0100
Commit message: Release 7.4.5 (#3043)


Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 241443f2b..3aaf11bf9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,14 @@
 # Change Log
 
 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
 * Fix failure to strip the Cookie header on change in host or HTTP downgrade
 
diff --git a/README.md b/README.md
index c96b246eb..f287fa98d 100644
--- a/README.md
+++ b/README.md
@@ -42,11 +42,11 @@ $promise->wait();
 
 We use GitHub issues only to discuss bugs and new features. For support please refer to:
 
 - [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)
 
 
 ## Installing Guzzle
 
@@ -71,14 +71,14 @@ composer require guzzlehttp/guzzle
 [guzzle-3-repo]: https://github.com/guzzle/guzzle3
 [guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x
 [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
 
 If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/guzzle/security/policy) for more information.
diff --git a/composer.json b/composer.json
index 9d60de424..7e043b038 100644
--- a/composer.json
+++ b/composer.json
@@ -52,11 +52,11 @@
     ],
     "require": {
         "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"
     },
     "provide": {
         "psr/http-client-implementation": "1.0"
diff --git a/src/RedirectMiddleware.php b/src/RedirectMiddleware.php
index cedad4e77..f67d448be 100644
--- a/src/RedirectMiddleware.php
+++ b/src/RedirectMiddleware.php
@@ -86,14 +86,12 @@ class RedirectMiddleware
         }
 
         $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]
             );
         }
@@ -196,42 +194,19 @@ class RedirectMiddleware
             $modify['set_headers']['Referer'] = (string) $uri;
         } else {
             $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.
      */
     private static function redirectUri(
         RequestInterface $request,
diff --git a/tests/RedirectMiddlewareTest.php b/tests/RedirectMiddlewareTest.php
index 3378d241d..5ca36ccb7 100644
--- a/tests/RedirectMiddlewareTest.php
+++ b/tests/RedirectMiddlewareTest.php
@@ -270,69 +270,109 @@ class RedirectMiddlewareTest extends TestCase
         ]);
         $promise->wait();
         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]),
                     'curl options still contain CURLOPT_HTTPAUTH entry'
                 );
@@ -343,11 +383,11 @@ class RedirectMiddlewareTest extends TestCase
                 return new Response(200);
             }
         ]);
         $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]]);
     }
 
     /**
      * @testWith ["digest"]
      *           ["ntlm"]
@@ -375,10 +415,65 @@ class RedirectMiddlewareTest extends TestCase
         $handler = HandlerStack::create($mock);
         $client = new Client(['handler' => $handler]);
         $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.
      *
      * @dataProvider modifyRequestFollowRequyestMethodAndBodyProvider

Vulnerable code: - [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](http://slack.httplug.io/)
[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/
        "guzzlehttp/psr7": "^1.8.3 || ^2.1",
        // If authorization is handled by curl, unset it if host is different.
        if ($request->getUri()->getHost() !== $nextRequest->getUri()->getHost()
            && defined('\CURLOPT_HTTPAUTH')
        ) {
        // Remove Authorization and Cookie headers if required.
        if (self::shouldStripSensitiveHeaders($request->getUri(), $modify['uri'])) {
    /**
     * 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;
    }

    public function crossOriginRedirectProvider()
        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],
        ];
     * @dataProvider crossOriginRedirectProvider
    public function testHeadersTreatmentOnRedirect($originalUri, $targetUri, $shouldBePresent)
        $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'));
        $client->get($originalUri, ['auth' => ['testuser', 'testpass'], 'headers' => ['Cookie' => 'foo=bar']]);
    public function testNotRemoveAuthorizationHeaderOnRedirect()
            new Response(302, ['Location' => 'http://example.com/2']),
            static function (RequestInterface $request) {
                self::assertTrue($request->hasHeader('Authorization'));
        $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass']]);
    public function testRemoveCurlAuthorizationOptionsOnRedirect($auth)
            new Response(302, ['Location' => 'http://test.com']),
        $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
Fixed code: ## 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

- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/)
[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/
        "guzzlehttp/psr7": "^1.9 || ^2.4",
        // If authorization is handled by curl, unset it if URI is cross-origin.
        if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $nextRequest->getUri()) && defined('\CURLOPT_HTTPAUTH')) {
        // Remove Authorization and Cookie headers if URI is cross-origin.
        if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $modify['uri'])) {
    /**
     * @testWith ["digest"]
     *           ["ntlm"]
     */
    public function testRemoveCurlAuthorizationOptionsOnRedirectCrossHost($auth)
        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]]);
     * @testWith ["digest"]
     *           ["ntlm"]
    public function testRemoveCurlAuthorizationOptionsOnRedirectCrossPort($auth)
        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'
                );
        $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
    /**
     * @testWith ["digest"]
     *           ["ntlm"]
     */
    public function testRemoveCurlAuthorizationOptionsOnRedirectCrossScheme($auth)
        if (!defined('\CURLOPT_HTTPAUTH')) {
            self::markTestSkipped('ext-curl is required for this test');
        }

            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'
                );
        $client->get('https://example.com?a=b', ['auth' => ['testuser', 'testpass', $auth]]);
    public function testRemoveCurlAuthorizationOptionsOnRedirectCrossSchemeSamePort($auth)
            new Response(302, ['Location' => 'http://example.com:80?a=b']),
        $client->get('https://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']]);
    }