CVE-2021-41268 (published 2021-11-24T19:15Z)

Symfony/SecurityBundle is the security system for Symfony, a PHP framework for web and console applications and a set of reusable PHP components. Since the rework of the Remember me cookie in version 5.3.0, the cookie is not invalidated when the user changes their password. Attackers can therefore maintain their access to the account even if the password is changed as long as they have had the chance to login once and get a valid remember me cookie. Starting with version 5.3.12, Symfony makes the password part of the signature by default. In that way, when the password changes, then the cookie is not valid anymore.

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

Weakness information:
cwe_id: CWE-384
cwe_name: Session Fixation
description: Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
extended_description: 
            Such a scenario is commonly observed when:
            
               A web application authenticates a user without first invalidating the existing session, thereby continuing to use the session already associated with the user.
               An attacker is able to force a known session identifier on a user so that, once the user authenticates, the attacker has access to the authenticated session.
               The application or container uses predictable session identifiers.
	    
	    In the generic exploit of session fixation vulnerabilities, an attacker creates a new session on a web application and records the associated session identifier. The attacker then causes the victim to associate, and possibly authenticate, against the server using that session identifier, giving the attacker access to the user's account through the active session.
         
         

Fix information:
repo_url: https://github.com/symfony/symfony
commit_details: repo_url: https://github.com/symfony/symfony
author: Wouter de Jong
author_date: 2021-10-23 13:11:11+02:00
msg: [SecurityBundle] Default signature_properties to the previous behavior
repository: repo_url: https://github.com/symfony/symfony
repo_name: symfony/symfony
description: The Symfony PHP framework
repo_language: PHP
owner: symfony
file_changes: filename: RememberMeFactory.php
change_type: ModificationType.MODIFY
diff: @@ -208,6 +208,7 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor
                 ->requiresAtLeastOneElement()
                 ->info('An array of properties on your User that are used to sign the remember-me cookie. If any of these change, all existing cookies will become invalid.')
                 ->example(['email', 'password'])
+                ->defaultValue(['password'])
             ->end()
             ->arrayNode('token_provider')
                 ->beforeNormalization()

programming_language: PHP
filename: UserChangingUserProvider.php
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,8 @@ class UserChangingUserProvider implements UserProviderInterface
 {
     private $inner;
 
+    public static $changePassword = false;
+
     public function __construct(InMemoryUserProvider $inner)
     {
         $this->inner = $inner;
@@ -28,26 +30,31 @@ class UserChangingUserProvider implements UserProviderInterface
 
     public function loadUserByUsername($username)
     {
-        return $this->inner->loadUserByUsername($username);
+        return $this->changeUser($this->inner->loadUserByUsername($username));
     }
 
     public function loadUserByIdentifier(string $userIdentifier): UserInterface
     {
-        return $this->inner->loadUserByIdentifier($userIdentifier);
+        return $this->changeUser($this->inner->loadUserByIdentifier($userIdentifier));
     }
 
     public function refreshUser(UserInterface $user)
     {
-        $user = $this->inner->refreshUser($user);
-
-        $alterUser = \Closure::bind(function (InMemoryUser $user) { $user->password = 'foo'; }, null, class_exists(User::class) ? User::class : InMemoryUser::class);
-        $alterUser($user);
-
-        return $user;
+        return $this->changeUser($this->inner->refreshUser($user));
     }
 
     public function supportsClass($class)
     {
         return $this->inner->supportsClass($class);
     }
+
+    private function changeUser(UserInterface $user): UserInterface
+    {
+        if (self::$changePassword) {
+            $alterUser = \Closure::bind(function (InMemoryUser $user) { $user->password = 'changed!'; }, null, class_exists(User::class) ? User::class : InMemoryUser::class);
+            $alterUser($user);
+        }
+
+        return $user;
+    }
 }

programming_language: PHP
filename: RememberMeTest.php
change_type: ModificationType.MODIFY
diff: @@ -11,8 +11,15 @@
 
 namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
 
+use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security\UserChangingUserProvider;
+
 class RememberMeTest extends AbstractWebTestCase
 {
+    protected function setUp(): void
+    {
+        UserChangingUserProvider::$changePassword = false;
+    }
+
     /**
      * @dataProvider provideConfigs
      */
@@ -51,11 +58,19 @@ class RememberMeTest extends AbstractWebTestCase
 
         $this->assertSame(302, $client->getResponse()->getStatusCode());
         $cookieJar = $client->getCookieJar();
-        $this->assertNotNull($cookieJar->get('REMEMBERME'));
+        $this->assertNotNull($cookie = $cookieJar->get('REMEMBERME'));
+
+        UserChangingUserProvider::$changePassword = true;
 
+        // change password (through user provider), this deauthenticates the session
         $client->request('GET', '/profile');
         $this->assertRedirect($client->getResponse(), '/login');
         $this->assertNull($cookieJar->get('REMEMBERME'));
+
+        // restore the old remember me cookie, it should no longer be valid
+        $cookieJar->set($cookie);
+        $client->request('GET', '/profile');
+        $this->assertRedirect($client->getResponse(), '/login');
     }
 
     public function testSessionLessRememberMeLogout()
@@ -121,11 +136,19 @@ class RememberMeTest extends AbstractWebTestCase
 
         $this->assertSame(302, $client->getResponse()->getStatusCode());
         $cookieJar = $client->getCookieJar();
-        $this->assertNotNull($cookieJar->get('REMEMBERME'));
+        $this->assertNotNull($cookie = $cookieJar->get('REMEMBERME'));
+
+        UserChangingUserProvider::$changePassword = true;
 
+        // change password (through user provider), this deauthenticates the session
         $client->request('GET', '/profile');
         $this->assertRedirect($client->getResponse(), '/login');
         $this->assertNull($cookieJar->get('REMEMBERME'));
+
+        // restore the old remember me cookie, it should no longer be valid
+        $cookieJar->set($cookie);
+        $client->request('GET', '/profile');
+        $this->assertRedirect($client->getResponse(), '/login');
     }
 
     /**

programming_language: PHP