CVE-2022-31042: Improper Removal of Sensitive Information Before Storage or Transfer (CWE-212)
value: Guzzle is an open source PHP HTTP client. In affected versions the `Cookie` headers on requests are sensitive information. On making a request using the `https` scheme to a server which responds with a redirect to a URI with the `http` scheme, or on making a request to a server which responds with a redirect to a a URI to a different host, we should not forward the `Cookie` header on. Prior to this fix, only cookies that were managed by our cookie middleware would be safely removed, and any `Cookie` header manually added to the initial request would not be stripped. We now always strip it, and allow the cookie middleware to re-add any cookies that it deems should be there. Affected Guzzle 7 users should upgrade to Guzzle 7.4.4 as soon as possible. Affected users using any earlier series of Guzzle should upgrade to Guzzle 6.5.7 or 7.4.4. Users unable to upgrade may consider an alternative approach to use your own redirect middleware, rather than ours. If you do not require or expect redirects to be followed, one should simply disable redirects all together.
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.5
Published: 2022-06-10T00:15Z
Repository: https://github.com/guzzle/guzzle
Language: PHP
Files changed: CHANGELOG.md, src/RedirectMiddleware.php, tests/RedirectMiddlewareTest.php
Commit date: 2022-06-09 22:39:15 +0100
Commit message: Release 7.4.4 (#3023)


Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d19ea86e..241443f2b 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.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
+
 ## 7.4.3 - 2022-05-25
 
 * Fix cross-domain cookie leakage
 
 ## 7.4.2 - 2022-03-20
diff --git a/src/RedirectMiddleware.php b/src/RedirectMiddleware.php
index 89c06526b..cedad4e77 100644
--- a/src/RedirectMiddleware.php
+++ b/src/RedirectMiddleware.php
@@ -140,11 +140,11 @@ class RedirectMiddleware
             }
         );
     }
 
     /**
-     * Check for too many redirects
+     * Check for too many redirects.
      *
      * @throws TooManyRedirectsException Too many redirects.
      */
     private function guardMax(RequestInterface $request, ResponseInterface $response, array &$options): void
     {
@@ -176,11 +176,11 @@ class RedirectMiddleware
 
             $modify['method'] = in_array($requestMethod, $safeMethods) ? $requestMethod : 'GET';
             $modify['body'] = '';
         }
 
-        $uri = $this->redirectUri($request, $response, $protocols);
+        $uri = self::redirectUri($request, $response, $protocols);
         if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) {
             $idnOptions = ($options['idn_conversion'] === true) ? \IDNA_DEFAULT : $options['idn_conversion'];
             $uri = Utils::idnUriConvert($uri, $idnOptions);
         }
 
@@ -196,23 +196,50 @@ class RedirectMiddleware
             $modify['set_headers']['Referer'] = (string) $uri;
         } else {
             $modify['remove_headers'][] = 'Referer';
         }
 
-        // Remove Authorization header if host is different.
-        if ($request->getUri()->getHost() !== $modify['uri']->getHost()) {
+        // Remove Authorization and Cookie headers if required.
+        if (self::shouldStripSensitiveHeaders($request->getUri(), $modify['uri'])) {
             $modify['remove_headers'][] = 'Authorization';
+            $modify['remove_headers'][] = 'Cookie';
         }
 
         return Psr7\Utils::modifyRequest($request, $modify);
     }
 
     /**
-     * Set the appropriate URL on the request based on the location header
+     * 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 function redirectUri(RequestInterface $request, ResponseInterface $response, array $protocols): UriInterface
-    {
+    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,
+        ResponseInterface $response,
+        array $protocols
+    ): UriInterface {
         $location = Psr7\UriResolver::resolve(
             $request->getUri(),
             new Psr7\Uri($response->getHeaderLine('Location'))
         );
 
diff --git a/tests/RedirectMiddlewareTest.php b/tests/RedirectMiddlewareTest.php
index 1e0841a27..3378d241d 100644
--- a/tests/RedirectMiddlewareTest.php
+++ b/tests/RedirectMiddlewareTest.php
@@ -270,22 +270,41 @@ class RedirectMiddlewareTest extends TestCase
         ]);
         $promise->wait();
         self::assertTrue($call);
     }
 
-    public function testRemoveAuthorizationHeaderOnRedirect()
+    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' => 'http://test.com']),
-            static function (RequestInterface $request) {
-                self::assertFalse($request->hasHeader('Authorization'));
+            new Response(302, ['Location' => $targetUri]),
+            static function (RequestInterface $request) use ($shouldBePresent) {
+                self::assertSame($shouldBePresent, $request->hasHeader('Authorization'));
+                self::assertSame($shouldBePresent, $request->hasHeader('Cookie'));
+
                 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($originalUri, ['auth' => ['testuser', 'testpass'], 'headers' => ['Cookie' => 'foo=bar']]);
     }
 
     public function testNotRemoveAuthorizationHeaderOnRedirect()
     {
         $mock = new MockHandler([

Vulnerable code:      * Check for too many redirects
        $uri = $this->redirectUri($request, $response, $protocols);
        // Remove Authorization header if host is different.
        if ($request->getUri()->getHost() !== $modify['uri']->getHost()) {
     * Set the appropriate URL on the request based on the location header
    private function redirectUri(RequestInterface $request, ResponseInterface $response, array $protocols): UriInterface
    {
    public function testRemoveAuthorizationHeaderOnRedirect()
            new Response(302, ['Location' => 'http://test.com']),
            static function (RequestInterface $request) {
                self::assertFalse($request->hasHeader('Authorization'));
        $client->get('http://example.com?a=b', ['auth' => ['testuser', 'testpass']]);
Fixed code: ## 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

     * Check for too many redirects.
        $uri = self::redirectUri($request, $response, $protocols);
        // Remove Authorization and Cookie headers if required.
        if (self::shouldStripSensitiveHeaders($request->getUri(), $modify['uri'])) {
            $modify['remove_headers'][] = 'Cookie';
     * 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,
        ResponseInterface $response,
        array $protocols
    ): UriInterface {
    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)
            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']]);