CVE-2022-29248 (published 2022-05-25T18:15Z)

Guzzle is a PHP HTTP client. Guzzle prior to versions 6.5.6 and 7.4.3 contains a vulnerability with the cookie middleware. The vulnerability is that it is not checked if the cookie domain equals the domain of the server which sets the cookie via the Set-Cookie header, allowing a malicious server to set cookies for unrelated domains. The cookie middleware is disabled by default, so most library consumers will not be affected by this issue. Only those who manually add the cookie middleware to the handler stack or construct the client with ['cookies' => true] are affected. Moreover, those who do not use the same Guzzle client to call multiple domains and have disabled redirect forwarding are not affected by this vulnerability. Guzzle versions 6.5.6 and 7.4.3 contain a patch for this issue. As a workaround, turn off the cookie middleware.

Severity: MEDIUM
CVSS v2 base score: 5.8 (AV:N/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 8.1, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N)

Weakness information:
cwe_id: CWE-565
cwe_name: Reliance on Cookies without Validation and Integrity Checking
description: The product relies on the existence or values of cookies when performing security-critical operations, but it does not properly ensure that the setting is valid for the associated user.
extended_description: Attackers can easily modify cookies, within the browser or by implementing the client-side code outside of the browser. Reliance on cookies without detailed validation and integrity checking can allow attackers to bypass authentication, conduct injection attacks such as SQL injection and cross-site scripting, or otherwise modify inputs in unexpected ways.
         

Fix information:
repo_url: https://github.com/guzzle/guzzle
commit_details: repo_url: https://github.com/guzzle/guzzle
author: Graham Campbell
author_date: 2022-05-25 14:24:33+01:00
msg: [7.x] Fix cross-domain cookie leakage (#3018)

Co-authored-by: Tim Düsterhus <209270+TimWolla@users.noreply.github.com>
repository: repo_url: https://github.com/guzzle/guzzle
repo_name: guzzle/guzzle
description: Guzzle, an extensible PHP HTTP client
repo_language: PHP
owner: guzzle
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,10 @@
 
 Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version.
 
+## 7.4.3 - 2022-05-25
+
+* Fix cross-domain cookie leakage
+
 ## 7.4.2 - 2022-03-20
 
 ### Fixed

programming_language: Markdown
filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -60,13 +60,13 @@ composer require guzzlehttp/guzzle
 
 ## Version Guidance
 
-| Version | Status     | Packagist           | Namespace    | Repo                | Docs                | PSR-7 | PHP Version |
-|---------|------------|---------------------|--------------|---------------------|---------------------|-------|-------------|
-| 3.x     | EOL        | `guzzle/guzzle`     | `Guzzle`     | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No    | >= 5.3.3    |
-| 4.x     | EOL        | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A                 | No    | >= 5.4      |
-| 5.x     | EOL        | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No    | >= 5.4      |
-| 6.x     | Security fixes | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes   | >= 5.5      |
-| 7.x     | Latest     | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes   | >= 7.2      |
+| Version | Status         | Packagist           | Namespace    | Repo                | Docs                | PSR-7 | PHP Version  |
+|---------|----------------|---------------------|--------------|---------------------|---------------------|-------|--------------|
+| 3.x     | EOL            | `guzzle/guzzle`     | `Guzzle`     | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No    | >=5.3.3,<7.0 |
+| 4.x     | EOL            | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A                 | No    | >=5.4,<7.0   |
+| 5.x     | EOL            | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No    | >=5.4,<7.4   |
+| 6.x     | Security fixes | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes   | >=5.5,<8.0   |
+| 7.x     | Latest         | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes   | >=7.2.5,<8.2 |
 
 [guzzle-3-repo]: https://github.com/guzzle/guzzle3
 [guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x

programming_language: Markdown
filename: CookieJar.php
change_type: ModificationType.MODIFY
diff: @@ -241,6 +241,11 @@ class CookieJar implements CookieJarInterface
                 if (0 !== \strpos($sc->getPath(), '/')) {
                     $sc->setPath($this->getCookiePathFromRequest($request));
                 }
+                if (!$sc->matchesDomain($request->getUri()->getHost())) {
+                    continue;
+                }
+                // Note: At this point `$sc->getDomain()` being a public suffix should
+                // be rejected, but we don't want to pull in the full PSL dependency.
                 $this->setCookie($sc);
             }
         }

programming_language: PHP
filename: SetCookie.php
change_type: ModificationType.MODIFY
diff: @@ -379,10 +379,12 @@ class SetCookie
 
         // Remove the leading '.' as per spec in RFC 6265.
         // https://tools.ietf.org/html/rfc6265#section-5.2.3
-        $cookieDomain = \ltrim($cookieDomain, '.');
+        $cookieDomain = \ltrim(\strtolower($cookieDomain), '.');
+
+        $domain = \strtolower($domain);
 
         // Domain not set or exact match.
-        if (!$cookieDomain || !\strcasecmp($domain, $cookieDomain)) {
+        if ('' === $cookieDomain || $domain === $cookieDomain) {
             return true;
         }
 

programming_language: PHP
filename: CookieJarTest.php
change_type: ModificationType.MODIFY
diff: @@ -271,7 +271,7 @@ class CookieJarTest extends TestCase
     /**
      * @dataProvider getMatchingCookiesDataProvider
      */
-    public function testReturnsCookiesMatchingRequests($url, $cookies)
+    public function testReturnsCookiesMatchingRequests(string $url, string $cookies)
     {
         $bag = [
             new SetCookie([
@@ -386,16 +386,13 @@ class CookieJarTest extends TestCase
             ['/foo', '/'],
             ['/foo/bar', '/foo'],
             ['/foo/bar/', '/foo/bar'],
-            ['foo', '/'],
-            ['foo/bar', '/'],
-            ['foo/bar/', '/'],
         ];
     }
 
     /**
      * @dataProvider getCookiePathsDataProvider
      */
-    public function testCookiePathWithEmptySetCookiePath($uriPath, $cookiePath)
+    public function testCookiePathWithEmptySetCookiePath(string $uriPath, string $cookiePath)
     {
         $response = (new Response(200))
             ->withAddedHeader(
@@ -407,13 +404,45 @@ class CookieJarTest extends TestCase
                 "bar=foo; expires={$this->futureExpirationDate()}; domain=www.example.com; path=foobar;"
             )
         ;
-        $request = (new Request('GET', $uriPath))->withHeader('Host', 'www.example.com');
+        $request = (new Request('GET', "https://www.example.com{$uriPath}"));
         $this->jar->extractCookies($request, $response);
 
         self::assertSame($cookiePath, $this->jar->toArray()[0]['Path']);
         self::assertSame($cookiePath, $this->jar->toArray()[1]['Path']);
     }
 
+    public function getDomainMatchesProvider()
+    {
+        return [
+            ['www.example.com', 'www.example.com', true],
+            ['www.example.com', 'www.EXAMPLE.com', true],
+            ['www.example.com', 'www.example.net', false],
+            ['www.example.com', 'ftp.example.com', false],
+            ['www.example.com', 'example.com', true],
+            ['www.example.com', 'EXAMPLE.com', true],
+            ['fra.de.example.com', 'EXAMPLE.com', true],
+            ['www.EXAMPLE.com', 'www.example.com', true],
+            ['www.EXAMPLE.com', 'www.example.COM', true],
+        ];
+    }
+
+    /**
+     * @dataProvider getDomainMatchesProvider
+     */
+    public function testIgnoresCookiesForMismatchingDomains(string $requestHost, string $domainAttribute, bool $matches)
+    {
+        $response = (new Response(200))
+            ->withAddedHeader(
+                'Set-Cookie',
+                "foo=bar; expires={$this->futureExpirationDate()}; domain={$domainAttribute}; path=/;"
+            )
+        ;
+        $request = (new Request('GET', "https://{$requestHost}/"));
+        $this->jar->extractCookies($request, $response);
+
+        self::assertCount($matches ? 1 : 0, $this->jar->toArray());
+    }
+
     private function futureExpirationDate()
     {
         return (new DateTimeImmutable)->add(new DateInterval('P1D'))->format(DateTime::COOKIE);

programming_language: PHP