From 17b29edd14ecddf23bde22a6d225fff6a97cd982 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 17 May 2026 22:36:33 +0200 Subject: [PATCH] fix(auth): rotate session ID on login to prevent session fixation (CWE-384) Inject Spring Security's SessionAuthenticationStrategy (ChangeSessionIdAuthenticationStrategy) into AuthSessionController and invoke onAuthentication at the credential boundary. The strategy calls HttpServletRequest.changeSessionId() to invalidate any pre-auth session ID an attacker may have planted and mint a fresh ID before the SecurityContext is attached. Addresses PR #612 / Nora B1. Co-Authored-By: Claude Opus 4.7 --- .../auth/AuthSessionController.java | 17 +++++++++++---- .../security/SecurityConfig.java | 10 +++++++++ .../auth/AuthSessionControllerTest.java | 21 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/auth/AuthSessionController.java b/backend/src/main/java/org/raddatz/familienarchiv/auth/AuthSessionController.java index 8eca4877..f9ba62ad 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/auth/AuthSessionController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/auth/AuthSessionController.java @@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.web.bind.annotation.*; @@ -20,23 +21,31 @@ import org.springframework.web.bind.annotation.*; public class AuthSessionController { private final AuthService authService; + private final SessionAuthenticationStrategy sessionAuthenticationStrategy; @PostMapping("/login") public ResponseEntity login( @RequestBody LoginRequest request, - HttpServletRequest httpRequest) { + HttpServletRequest httpRequest, + HttpServletResponse httpResponse) { String ip = resolveClientIp(httpRequest); String ua = resolveUserAgent(httpRequest); AuthService.LoginResult result = authService.login(request.email(), request.password(), ip, ua); - // Establish server-side session. Spring Session JDBC intercepts getSession().setAttribute() - // and persists the record; the Set-Cookie: fa_session= is added automatically. + // Session-fixation defense (CWE-384): rotate the session ID at the authentication + // boundary. ChangeSessionIdAuthenticationStrategy invalidates any pre-auth session ID + // an attacker may have planted and mints a fresh one before we attach the SecurityContext. + httpRequest.getSession(true); + sessionAuthenticationStrategy.onAuthentication(result.authentication(), httpRequest, httpResponse); + + // Spring Session JDBC intercepts setAttribute() and persists the record under the + // (now rotated) opaque ID; the Set-Cookie: fa_session= is added automatically. SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(result.authentication()); SecurityContextHolder.setContext(context); - httpRequest.getSession(true) + httpRequest.getSession() .setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context); return ResponseEntity.ok(result.user()); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java b/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java index 8828e610..80747a8f 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java @@ -17,6 +17,8 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; @Configuration @EnableWebSecurity @@ -43,6 +45,14 @@ public class SecurityConfig { return config.getAuthenticationManager(); } + @Bean + public SessionAuthenticationStrategy sessionAuthenticationStrategy() { + // ChangeSessionIdAuthenticationStrategy rotates the session ID via the Servlet 3.1+ + // HttpServletRequest.changeSessionId() — preserves attributes, mints a fresh ID. + // Used by AuthSessionController.login to defend against session fixation (CWE-384). + return new ChangeSessionIdAuthenticationStrategy(); + } + @Bean @Order(1) public SecurityFilterChain managementFilterChain(HttpSecurity http) throws Exception { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java index 037f1ec8..d948dfab 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java @@ -14,12 +14,14 @@ import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -33,6 +35,7 @@ class AuthSessionControllerTest { @MockitoBean AuthService authService; @MockitoBean CustomUserDetailsService customUserDetailsService; + @MockitoBean SessionAuthenticationStrategy sessionAuthenticationStrategy; // ─── POST /api/auth/login ────────────────────────────────────────────────── @@ -79,6 +82,24 @@ class AuthSessionControllerTest { .andExpect(status().isOk()); } + @Test + void login_delegates_to_SessionAuthenticationStrategy_for_fixation_protection() throws Exception { + UUID userId = UUID.randomUUID(); + AppUser appUser = AppUser.builder().id(userId).email("fix@test.de").build(); + Authentication auth = mock(Authentication.class); + when(authService.login(anyString(), anyString(), anyString(), anyString())) + .thenReturn(new LoginResult(appUser, auth)); + + mockMvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"email\":\"fix@test.de\",\"password\":\"pass\"}")) + .andExpect(status().isOk()); + + // Session-fixation defense (CWE-384): the controller must hand the new + // Authentication to Spring Security's strategy, which rotates the session ID. + verify(sessionAuthenticationStrategy).onAuthentication(eq(auth), any(), any()); + } + @Test void login_does_not_set_cookie_on_failure() throws Exception { when(authService.login(anyString(), anyString(), anyString(), anyString()))