From bfe267072251d570e9fe7b6e8f5e9fe4105aecf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Isaert?= Date: Wed, 31 Jan 2024 14:12:26 +0100 Subject: [PATCH] [HttpFoundation] Fix clearing CHIPS cookies --- .github/expected-missing-return-types.diff | 26 +++++++-------- .../DependencyInjection/MainConfiguration.php | 1 + .../MainConfigurationTest.php | 33 +++++++++++++++++++ .../HttpFoundation/ResponseHeaderBag.php | 8 +++-- .../Tests/ResponseHeaderBagTest.php | 8 +++++ .../CookieClearingLogoutListener.php | 2 +- .../CookieClearingLogoutListenerTest.php | 4 ++- 7 files changed, 65 insertions(+), 17 deletions(-) diff --git a/.github/expected-missing-return-types.diff b/.github/expected-missing-return-types.diff index 63b2d15b90800..0aee6685a711f 100644 --- a/.github/expected-missing-return-types.diff +++ b/.github/expected-missing-return-types.diff @@ -107,14 +107,14 @@ diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExt + protected function registerMappingDrivers(array $objectManager, ContainerBuilder $container): void { // configure metadata driver for each bundle based on the type of mapping files found -@@ -238,5 +238,5 @@ abstract class AbstractDoctrineExtension extends Extension +@@ -240,5 +240,5 @@ abstract class AbstractDoctrineExtension extends Extension * @throws \InvalidArgumentException */ - protected function assertValidMappingConfiguration(array $mappingConfig, string $objectManagerName) + protected function assertValidMappingConfiguration(array $mappingConfig, string $objectManagerName): void { if (!$mappingConfig['type'] || !$mappingConfig['dir'] || !$mappingConfig['prefix']) { -@@ -328,5 +328,5 @@ abstract class AbstractDoctrineExtension extends Extension +@@ -330,5 +330,5 @@ abstract class AbstractDoctrineExtension extends Extension * @throws \InvalidArgumentException in case of unknown driver type */ - protected function loadObjectManagerCacheDriver(array $objectManager, ContainerBuilder $container, string $cacheName) @@ -1942,8 +1942,8 @@ diff --git a/src/Symfony/Component/Config/Exception/LoaderLoadException.php b/sr diff --git a/src/Symfony/Component/Config/FileLocator.php b/src/Symfony/Component/Config/FileLocator.php --- a/src/Symfony/Component/Config/FileLocator.php +++ b/src/Symfony/Component/Config/FileLocator.php -@@ -34,5 +34,5 @@ class FileLocator implements FileLocatorInterface - * @return string|array +@@ -36,5 +36,5 @@ class FileLocator implements FileLocatorInterface + * @psalm-return ($first is true ? string : string[]) */ - public function locate(string $name, ?string $currentPath = null, bool $first = true) + public function locate(string $name, ?string $currentPath = null, bool $first = true): string|array @@ -1952,8 +1952,8 @@ diff --git a/src/Symfony/Component/Config/FileLocator.php b/src/Symfony/Componen diff --git a/src/Symfony/Component/Config/FileLocatorInterface.php b/src/Symfony/Component/Config/FileLocatorInterface.php --- a/src/Symfony/Component/Config/FileLocatorInterface.php +++ b/src/Symfony/Component/Config/FileLocatorInterface.php -@@ -31,4 +31,4 @@ interface FileLocatorInterface - * @throws FileLocatorFileNotFoundException If a file is not found +@@ -33,4 +33,4 @@ interface FileLocatorInterface + * @psalm-return ($first is true ? string : string[]) */ - public function locate(string $name, ?string $currentPath = null, bool $first = true); + public function locate(string $name, ?string $currentPath = null, bool $first = true): string|array; @@ -3776,7 +3776,7 @@ diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ReplaceAliasByAc diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php -@@ -38,5 +38,5 @@ class ResolveBindingsPass extends AbstractRecursivePass +@@ -39,5 +39,5 @@ class ResolveBindingsPass extends AbstractRecursivePass * @return void */ - public function process(ContainerBuilder $container) @@ -4088,7 +4088,7 @@ diff --git a/src/Symfony/Component/DependencyInjection/ContainerInterface.php b/ diff --git a/src/Symfony/Component/DependencyInjection/Exception/AutowiringFailedException.php b/src/Symfony/Component/DependencyInjection/Exception/AutowiringFailedException.php --- a/src/Symfony/Component/DependencyInjection/Exception/AutowiringFailedException.php +++ b/src/Symfony/Component/DependencyInjection/Exception/AutowiringFailedException.php -@@ -71,5 +71,5 @@ class AutowiringFailedException extends RuntimeException +@@ -69,5 +69,5 @@ class AutowiringFailedException extends RuntimeException * @return string */ - public function getServiceId() @@ -7449,14 +7449,14 @@ diff --git a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php b/src/Sy + public function removeCookie(string $name, ?string $path = '/', ?string $domain = null): void { $path ??= '/'; -@@ -237,5 +237,5 @@ class ResponseHeaderBag extends HeaderBag +@@ -239,5 +239,5 @@ class ResponseHeaderBag extends HeaderBag * @return void */ -- public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null) -+ public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null): void +- public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null /* , bool $partitioned = false */) ++ public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null /* , bool $partitioned = false */): void { - $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite)); -@@ -247,5 +247,5 @@ class ResponseHeaderBag extends HeaderBag + $partitioned = 6 < \func_num_args() ? \func_get_arg(6) : false; +@@ -251,5 +251,5 @@ class ResponseHeaderBag extends HeaderBag * @return string */ - public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '') diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index ac9fd2a1e9e3d..b2eabca0a7fe0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -281,6 +281,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('domain')->defaultNull()->end() ->scalarNode('secure')->defaultFalse()->end() ->scalarNode('samesite')->defaultNull()->end() + ->scalarNode('partitioned')->defaultFalse()->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php index 52a392fe870f7..8d3fed44695d2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php @@ -141,6 +141,39 @@ public function testLogoutCsrf() } } + public function testLogoutDeleteCookies() + { + $config = [ + 'firewalls' => [ + 'stub' => [ + 'logout' => [ + 'delete_cookies' => [ + 'my_cookie' => [ + 'path' => '/', + 'domain' => 'example.org', + 'secure' => true, + 'samesite' => 'none', + 'partitioned' => true, + ], + ], + ], + ], + ], + ]; + $config = array_merge(static::$minimalConfig, $config); + + $processor = new Processor(); + $configuration = new MainConfiguration([], []); + $processedConfig = $processor->processConfiguration($configuration, [$config]); + $this->assertArrayHasKey('delete_cookies', $processedConfig['firewalls']['stub']['logout']); + $deleteCookies = $processedConfig['firewalls']['stub']['logout']['delete_cookies']; + $this->assertSame('/', $deleteCookies['my_cookie']['path']); + $this->assertSame('example.org', $deleteCookies['my_cookie']['domain']); + $this->assertTrue($deleteCookies['my_cookie']['secure']); + $this->assertSame('none', $deleteCookies['my_cookie']['samesite']); + $this->assertTrue($deleteCookies['my_cookie']['partitioned']); + } + public function testDefaultUserCheckers() { $processor = new Processor(); diff --git a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php index 80d267553a587..376357d01f902 100644 --- a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php @@ -234,11 +234,15 @@ public function getCookies(string $format = self::COOKIES_FLAT): array /** * Clears a cookie in the browser. * + * @param bool $partitioned + * * @return void */ - public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null) + public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null /* , bool $partitioned = false */) { - $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite)); + $partitioned = 6 < \func_num_args() ? \func_get_arg(6) : false; + + $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite, $partitioned)); } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php index 8165e43740a66..9e61dd684e60f 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php @@ -136,6 +136,14 @@ public function testClearCookieSamesite() $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d M Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure; samesite=none', $bag); } + public function testClearCookiePartitioned() + { + $bag = new ResponseHeaderBag([]); + + $bag->clearCookie('foo', '/', null, true, false, 'none', true); + $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d M Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure; samesite=none; partitioned', $bag); + } + public function testReplace() { $bag = new ResponseHeaderBag([]); diff --git a/src/Symfony/Component/Security/Http/EventListener/CookieClearingLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/CookieClearingLogoutListener.php index 345e8d8b6ace4..cbc85990fafa7 100644 --- a/src/Symfony/Component/Security/Http/EventListener/CookieClearingLogoutListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/CookieClearingLogoutListener.php @@ -40,7 +40,7 @@ public function onLogout(LogoutEvent $event): void } foreach ($this->cookies as $cookieName => $cookieData) { - $response->headers->clearCookie($cookieName, $cookieData['path'], $cookieData['domain'], $cookieData['secure'] ?? false, true, $cookieData['samesite'] ?? null); + $response->headers->clearCookie($cookieName, $cookieData['path'], $cookieData['domain'], $cookieData['secure'] ?? false, true, $cookieData['samesite'] ?? null, $cookieData['partitioned'] ?? false); } } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CookieClearingLogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CookieClearingLogoutListenerTest.php index f4c0e3d89b611..f38478e72d01e 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/CookieClearingLogoutListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CookieClearingLogoutListenerTest.php @@ -27,7 +27,7 @@ public function testLogout() $event = new LogoutEvent(new Request(), null); $event->setResponse($response); - $listener = new CookieClearingLogoutListener(['foo' => ['path' => '/foo', 'domain' => 'foo.foo', 'secure' => true, 'samesite' => Cookie::SAMESITE_STRICT], 'foo2' => ['path' => null, 'domain' => null]]); + $listener = new CookieClearingLogoutListener(['foo' => ['path' => '/foo', 'domain' => 'foo.foo', 'secure' => true, 'samesite' => Cookie::SAMESITE_STRICT, 'partitioned' => true], 'foo2' => ['path' => null, 'domain' => null]]); $cookies = $response->headers->getCookies(); $this->assertCount(0, $cookies); @@ -43,6 +43,7 @@ public function testLogout() $this->assertEquals('foo.foo', $cookie->getDomain()); $this->assertEquals(Cookie::SAMESITE_STRICT, $cookie->getSameSite()); $this->assertTrue($cookie->isSecure()); + $this->assertTrue($cookie->isPartitioned()); $this->assertTrue($cookie->isCleared()); $cookie = $cookies['']['/']['foo2']; @@ -51,6 +52,7 @@ public function testLogout() $this->assertNull($cookie->getDomain()); $this->assertNull($cookie->getSameSite()); $this->assertFalse($cookie->isSecure()); + $this->assertFalse($cookie->isPartitioned()); $this->assertTrue($cookie->isCleared()); } }