CVE-2024-23679 (published 2024-01-19T21:15Z)

Enonic XP versions less than 7.7.4 are vulnerable to a session fixation issue. An remote and unauthenticated attacker can use prior sessions due to the lack of invalidating session attributes.\n\n

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/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/enonic/xp
commit_details: repo_url: https://github.com/enonic/xp
author: Bradnitski
author_date: 2021-12-03 15:31:24+03:00
msg: Invalidate old session after login #9253
repository: repo_url: https://github.com/enonic/xp
repo_name: enonic/xp
description: Enonic XP
repo_language: Java
owner: enonic
file_changes: filename: LoginHandler.java
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,7 @@ import javax.servlet.http.HttpSession;
 
 import com.enonic.xp.context.Context;
 import com.enonic.xp.context.ContextBuilder;
+import com.enonic.xp.context.LocalScope;
 import com.enonic.xp.portal.PortalRequest;
 import com.enonic.xp.script.bean.BeanContext;
 import com.enonic.xp.script.bean.ScriptBean;
@@ -30,11 +31,6 @@ import com.enonic.xp.session.Session;
 public final class LoginHandler
     implements ScriptBean
 {
-    private enum Scope
-    {
-        SESSION, REQUEST, NONE
-    }
-
     private String user;
 
     private String password;
@@ -113,15 +109,26 @@ public final class LoginHandler
 
     private void createSession( final AuthenticationInfo authInfo )
     {
-        final Session session = this.context.get().getLocalScope().getSession();
+        final LocalScope localScope = this.context.get().getLocalScope();
+        final Session session = localScope.getSession();
+
         if ( session != null )
         {
-            session.setAttribute( authInfo );
-        }
+            final var attributes = session.getAttributes();
+            session.invalidate();
 
-        if ( this.sessionTimeout != null )
-        {
-            setSessionTimeout();
+            final Session newSession = localScope.getSession();
+
+            if ( newSession != null )
+            {
+                attributes.forEach( newSession::setAttribute );
+                session.setAttribute( authInfo );
+
+                if ( this.sessionTimeout != null )
+                {
+                    setSessionTimeout();
+                }
+            }
         }
     }
 
@@ -149,9 +156,8 @@ public final class LoginHandler
     private IdProviders getSortedIdProviders()
     {
         IdProviders idProviders = securityService.get().getIdProviders();
-        return IdProviders.from( idProviders.stream().
-            sorted( Comparator.comparing( u -> u.getKey().toString() ) ).
-            collect( Collectors.toList() ) );
+        return IdProviders.from(
+            idProviders.stream().sorted( Comparator.comparing( u -> u.getKey().toString() ) ).collect( Collectors.toList() ) );
     }
 
     private AuthenticationInfo attemptLogin()
@@ -221,11 +227,12 @@ public final class LoginHandler
     private <T> T runAsAuthenticated( Callable<T> runnable )
     {
         final AuthenticationInfo authInfo = AuthenticationInfo.create().principals( RoleKeys.AUTHENTICATED ).user( User.ANONYMOUS ).build();
-        return ContextBuilder.from( this.context.get() ).
-            authInfo( authInfo ).
-            repositoryId( SystemConstants.SYSTEM_REPO_ID ).
-            branch( SecurityConstants.BRANCH_SECURITY ).build().
-            callWith( runnable );
+        return ContextBuilder.from( this.context.get() )
+            .authInfo( authInfo )
+            .repositoryId( SystemConstants.SYSTEM_REPO_ID )
+            .branch( SecurityConstants.BRANCH_SECURITY )
+            .build()
+            .callWith( runnable );
     }
 
     private boolean isValidEmail( final String value )
@@ -253,4 +260,9 @@ public final class LoginHandler
         this.context = context.getBinding( Context.class );
         this.portalRequestSupplier = context.getBinding( PortalRequest.class );
     }
+
+    private enum Scope
+    {
+        SESSION, REQUEST, NONE
+    }
 }

programming_language: Java
filename: LoginHandlerTest.java
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,8 @@ import com.enonic.xp.testing.ScriptTestSupport;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 public class LoginHandlerTest
     extends ScriptTestSupport
@@ -192,6 +194,25 @@ public class LoginHandlerTest
         assertEquals( "idprovider3", matcher.loginIdProviderAttempts.get( 2 ).toString() );
     }
 
+    @Test
+    public void testSessionInvalidatedOnLogin()
+    {
+        final AuthenticationInfo authInfo = TestDataFixtures.createAuthenticationInfo();
+
+        final IdProviders idProviders =
+            IdProviders.from( IdProvider.create().displayName( "system" ).key( IdProviderKey.from( "system" ) ).build() );
+
+        Mockito.when( this.securityService.authenticate( Mockito.any() ) ).thenReturn( authInfo );
+        Mockito.when( this.securityService.getIdProviders() ).thenReturn( idProviders );
+
+        final SessionMock session = Mockito.spy( new SessionMock() );
+        ContextAccessor.current().getLocalScope().setSession( session );
+
+        runScript( "/lib/xp/examples/auth/login.js" );
+
+        verify( session, times( 5 ) ).invalidate();
+    }
+
     private static class AuthTokenMatcher
         implements ArgumentMatcher<AuthenticationToken>
     {

programming_language: Java
repo_url: https://github.com/enonic/xp
commit_details: repo_url: https://github.com/enonic/xp
author: Bradnitski
author_date: 2021-12-03 15:31:24+03:00
msg: Invalidate old session after login #9253

(cherry picked from commit 0189975691e9e6407a9fee87006f730e84f734ff)
repository: repo_url: https://github.com/enonic/xp
repo_name: enonic/xp
description: Enonic XP
repo_language: Java
owner: enonic
file_changes: filename: LoginHandler.java
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,7 @@ import javax.servlet.http.HttpSession;
 
 import com.enonic.xp.context.Context;
 import com.enonic.xp.context.ContextBuilder;
+import com.enonic.xp.context.LocalScope;
 import com.enonic.xp.portal.PortalRequest;
 import com.enonic.xp.script.bean.BeanContext;
 import com.enonic.xp.script.bean.ScriptBean;
@@ -30,11 +31,6 @@ import com.enonic.xp.session.Session;
 public final class LoginHandler
     implements ScriptBean
 {
-    private enum Scope
-    {
-        SESSION, REQUEST, NONE
-    }
-
     private String user;
 
     private String password;
@@ -113,15 +109,26 @@ public final class LoginHandler
 
     private void createSession( final AuthenticationInfo authInfo )
     {
-        final Session session = this.context.get().getLocalScope().getSession();
+        final LocalScope localScope = this.context.get().getLocalScope();
+        final Session session = localScope.getSession();
+
         if ( session != null )
         {
-            session.setAttribute( authInfo );
-        }
+            final var attributes = session.getAttributes();
+            session.invalidate();
 
-        if ( this.sessionTimeout != null )
-        {
-            setSessionTimeout();
+            final Session newSession = localScope.getSession();
+
+            if ( newSession != null )
+            {
+                attributes.forEach( newSession::setAttribute );
+                session.setAttribute( authInfo );
+
+                if ( this.sessionTimeout != null )
+                {
+                    setSessionTimeout();
+                }
+            }
         }
     }
 
@@ -149,9 +156,8 @@ public final class LoginHandler
     private IdProviders getSortedIdProviders()
     {
         IdProviders idProviders = securityService.get().getIdProviders();
-        return IdProviders.from( idProviders.stream().
-            sorted( Comparator.comparing( u -> u.getKey().toString() ) ).
-            collect( Collectors.toList() ) );
+        return IdProviders.from(
+            idProviders.stream().sorted( Comparator.comparing( u -> u.getKey().toString() ) ).collect( Collectors.toList() ) );
     }
 
     private AuthenticationInfo attemptLogin()
@@ -221,11 +227,12 @@ public final class LoginHandler
     private <T> T runAsAuthenticated( Callable<T> runnable )
     {
         final AuthenticationInfo authInfo = AuthenticationInfo.create().principals( RoleKeys.AUTHENTICATED ).user( User.ANONYMOUS ).build();
-        return ContextBuilder.from( this.context.get() ).
-            authInfo( authInfo ).
-            repositoryId( SystemConstants.SYSTEM_REPO_ID ).
-            branch( SecurityConstants.BRANCH_SECURITY ).build().
-            callWith( runnable );
+        return ContextBuilder.from( this.context.get() )
+            .authInfo( authInfo )
+            .repositoryId( SystemConstants.SYSTEM_REPO_ID )
+            .branch( SecurityConstants.BRANCH_SECURITY )
+            .build()
+            .callWith( runnable );
     }
 
     private boolean isValidEmail( final String value )
@@ -253,4 +260,9 @@ public final class LoginHandler
         this.context = context.getBinding( Context.class );
         this.portalRequestSupplier = context.getBinding( PortalRequest.class );
     }
+
+    private enum Scope
+    {
+        SESSION, REQUEST, NONE
+    }
 }

programming_language: Java
filename: LoginHandlerTest.java
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,8 @@ import com.enonic.xp.testing.ScriptTestSupport;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 public class LoginHandlerTest
     extends ScriptTestSupport
@@ -192,6 +194,25 @@ public class LoginHandlerTest
         assertEquals( "idprovider3", matcher.loginIdProviderAttempts.get( 2 ).toString() );
     }
 
+    @Test
+    public void testSessionInvalidatedOnLogin()
+    {
+        final AuthenticationInfo authInfo = TestDataFixtures.createAuthenticationInfo();
+
+        final IdProviders idProviders =
+            IdProviders.from( IdProvider.create().displayName( "system" ).key( IdProviderKey.from( "system" ) ).build() );
+
+        Mockito.when( this.securityService.authenticate( Mockito.any() ) ).thenReturn( authInfo );
+        Mockito.when( this.securityService.getIdProviders() ).thenReturn( idProviders );
+
+        final SessionMock session = Mockito.spy( new SessionMock() );
+        ContextAccessor.current().getLocalScope().setSession( session );
+
+        runScript( "/lib/xp/examples/auth/login.js" );
+
+        verify( session, times( 5 ) ).invalidate();
+    }
+
     private static class AuthTokenMatcher
         implements ArgumentMatcher<AuthenticationToken>
     {

programming_language: Java