diff --git a/core/src/main/java/hudson/security/SecurityRealm.java b/core/src/main/java/hudson/security/SecurityRealm.java index 1bda207fba..0e166c561d 100644 --- a/core/src/main/java/hudson/security/SecurityRealm.java +++ b/core/src/main/java/hudson/security/SecurityRealm.java @@ -678,7 +678,9 @@ public abstract class SecurityRealm implements Describable, Exten apf.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/loginError")); filters.add(apf); } - filters.add(new RememberMeAuthenticationFilter(sc.manager2, sc.rememberMe2)); + final RememberMeAuthenticationFilter rememberMe = new RememberMeAuthenticationFilter(sc.manager2, sc.rememberMe2); + rememberMe.setSessionAuthenticationStrategy(new SessionFixationProtectionStrategy()); + filters.add(rememberMe); filters.addAll(commonFilters()); return new ChainedServletFilter2(filters); } diff --git a/test/src/test/java/hudson/security/SecurityRealmSecurity2371Test.java b/test/src/test/java/hudson/security/SecurityRealmSecurity2371Test.java index cc8802a8f1..b03db9f4b3 100644 --- a/test/src/test/java/hudson/security/SecurityRealmSecurity2371Test.java +++ b/test/src/test/java/hudson/security/SecurityRealmSecurity2371Test.java @@ -37,9 +37,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; /** * Split from {@link SecurityRealmTest} because this is parameterized. @@ -114,4 +116,51 @@ class SecurityRealmSecurity2371Test { System.clearProperty(SecurityRealm.class.getName() + ".sessionFixationProtectionMode"); } } + + @Issue("SECURITY-4016") + @ParameterizedTest + @MethodSource("modes") + void testSessionChangeOnRememberMeAutoLogin(Integer mode) throws Exception { + if (mode != null) { + System.setProperty(SecurityRealm.class.getName() + ".sessionFixationProtectionMode", String.valueOf(mode)); + } + try { + HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null); + j.jenkins.setSecurityRealm(realm); + j.jenkins.setDisableRememberMe(false); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.READ).everywhere().toEveryone() + .grant(Jenkins.ADMINISTER).everywhere().to(USERNAME)); + realm.createAccount(USERNAME, USERNAME); + + // Phase 1: attacker prepares an anonymous session + JenkinsRule.WebClient attacker = j.createWebClient(); + attacker.goTo(""); // establishes anonymous session + Cookie attackerSession = attacker.getCookieManager().getCookie(SESSION_COOKIE_NAME); + + // Phase 2: victim logs in with remember-me + JenkinsRule.WebClient victim = j.createWebClient(); + victim.login(USERNAME, USERNAME, true); + Cookie rememberMeCookie = victim.getCookieManager() + .getCookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY); + + // Phase 3: victim's session-only cookie disappears (browser restart) + // but remember-me persists. Attacker plants their session ID. + JenkinsRule.WebClient browser = j.createWebClient(); + browser.getCookieManager().addCookie(attackerSession); // planted by sibling page + browser.getCookieManager().addCookie(rememberMeCookie); // survives browser restart + + // Phase 4: victim navigates to Jenkins — remember-me auto-login fires + browser.goTo(""); + Cookie postLoginSession = browser.getCookieManager().getCookie(SESSION_COOKIE_NAME); + + // The session must have been rotated + assertNotEquals(attackerSession.getValue(), postLoginSession.getValue()); + + // Phase 5: replaying the pre-authentication session must not grant access + assertThrows(FailingHttpStatusCodeException.class, () -> attacker.goTo("manage")); + } finally { + System.clearProperty(SecurityRealm.class.getName() + ".sessionFixationProtectionMode"); + } + } }