From: Nicolas Grekas <nicolas.grekas@gmail.com>
Date: Mon, 23 Jan 2023 19:43:46 +0100
Subject: [Security/Http] Remove CSRF tokens from storage on successful login
 [CVE-2022-24895]

Origin: backport, https://github.com/symfony/symfony/commit/c75c5699f02da5ebb92ca6424aeb0e7cac5703a4
---
 .../Bundle/SecurityBundle/Resources/config/security.xml    |  1 +
 .../SecurityBundle/Tests/Functional/CsrfFormLoginTest.php  |  6 ++++++
 .../Bundle/SecurityBundle/Tests/Functional/LogoutTest.php  |  4 +---
 .../Http/Session/SessionAuthenticationStrategy.php         | 14 +++++++++++---
 .../Tests/Session/SessionAuthenticationStrategyTest.php    | 13 +++++++++++++
 5 files changed, 32 insertions(+), 6 deletions(-)

diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
index 3491383..eabe5e5 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
@@ -63,6 +63,7 @@
 
         <service id="security.authentication.session_strategy" class="Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy">
             <argument>%security.authentication.session_strategy.strategy%</argument>
+            <argument type="service" id="security.csrf.token_storage" on-invalid="ignore" />
         </service>
         <service id="Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface" alias="security.authentication.session_strategy" />
 
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php
index 1a672d7..08ea67a 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php
@@ -19,12 +19,15 @@ class CsrfFormLoginTest extends AbstractWebTestCase
     public function testFormLoginAndLogoutWithCsrfTokens($config)
     {
         $client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]);
+        static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
 
         $form = $client->request('GET', '/login')->selectButton('login')->form();
         $form['user_login[username]'] = 'johannes';
         $form['user_login[password]'] = 'test';
         $client->submit($form);
 
+        $this->assertFalse(static::$container->get('security.csrf.token_storage')->hasToken('foo'));
+
         $this->assertRedirect($client->getResponse(), '/profile');
 
         $crawler = $client->followRedirect();
@@ -48,11 +51,14 @@ class CsrfFormLoginTest extends AbstractWebTestCase
     public function testFormLoginWithInvalidCsrfToken($config)
     {
         $client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]);
+        static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
 
         $form = $client->request('GET', '/login')->selectButton('login')->form();
         $form['user_login[_token]'] = '';
         $client->submit($form);
 
+        $this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo'));
+
         $this->assertRedirect($client->getResponse(), '/login');
 
         $text = $client->followRedirect()->text(null, true);
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
index cb7868f..465027f 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
@@ -36,15 +36,13 @@ class LogoutTest extends AbstractWebTestCase
     public function testCsrfTokensAreClearedOnLogout()
     {
         $client = $this->createClient(['test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml']);
-        static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
 
         $client->request('POST', '/login', [
             '_username' => 'johannes',
             '_password' => 'test',
         ]);
 
-        $this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo'));
-        $this->assertSame('bar', static::$container->get('security.csrf.token_storage')->getToken('foo'));
+        static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
 
         $client->request('GET', '/logout');
 
diff --git a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php
index a4bb888..7369105 100644
--- a/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php
+++ b/src/Symfony/Component/Security/Http/Session/SessionAuthenticationStrategy.php
@@ -13,6 +13,7 @@ namespace Symfony\Component\Security\Http\Session;
 
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
 
 /**
  * The default session strategy implementation.
@@ -31,10 +32,15 @@ class SessionAuthenticationStrategy implements SessionAuthenticationStrategyInte
     public const INVALIDATE = 'invalidate';
 
     private $strategy;
+    private $csrfTokenStorage = null;
 
-    public function __construct(string $strategy)
+    public function __construct(string $strategy, ClearableTokenStorageInterface $csrfTokenStorage = null)
     {
         $this->strategy = $strategy;
+
+        if (self::MIGRATE === $strategy) {
+            $this->csrfTokenStorage = $csrfTokenStorage;
+        }
     }
 
     /**
@@ -47,10 +53,12 @@ class SessionAuthenticationStrategy implements SessionAuthenticationStrategyInte
                 return;
 
             case self::MIGRATE:
-                // Note: this logic is duplicated in several authentication listeners
-                // until Symfony 5.0 due to a security fix with BC compat
                 $request->getSession()->migrate(true);
 
+                if ($this->csrfTokenStorage) {
+                    $this->csrfTokenStorage->clear();
+                }
+
                 return;
 
             case self::INVALIDATE:
diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php
index 94ff922..66550a2 100644
--- a/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php
@@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Session\SessionInterface;
 use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
 use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
 
 class SessionAuthenticationStrategyTest extends TestCase
@@ -57,6 +58,18 @@ class SessionAuthenticationStrategyTest extends TestCase
         $strategy->onAuthentication($this->getRequest($session), $this->getToken());
     }
 
+    public function testCsrfTokensAreCleared()
+    {
+        $session = $this->createMock(SessionInterface::class);
+        $session->expects($this->once())->method('migrate')->with($this->equalTo(true));
+
+        $csrfStorage = $this->createMock(ClearableTokenStorageInterface::class);
+        $csrfStorage->expects($this->once())->method('clear');
+
+        $strategy = new SessionAuthenticationStrategy(SessionAuthenticationStrategy::MIGRATE, $csrfStorage);
+        $strategy->onAuthentication($this->getRequest($session), $this->createMock(TokenInterface::class));
+    }
+
     private function getRequest($session = null)
     {
         $request = $this->createMock(Request::class);
