1
0
mirror of synced 2026-08-05 17:57:15 +00:00

SEC-2135: Support HttpServletRequest#changeSessionId()

This commit is contained in:
Rob Winch
2013-08-10 18:41:57 -05:00
parent 75fb971d23
commit 797df51264
10 changed files with 730 additions and 125 deletions
@@ -0,0 +1,136 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.session;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* A base class for performing session fixation protection.
*
* @author Rob Winch
* @since 3.2
*/
abstract class AbstractSessionFixationProtectionStrategy implements SessionAuthenticationStrategy, ApplicationEventPublisherAware {
protected final Log logger = LogFactory.getLog(this.getClass());
/**
* Used for publishing events related to session fixation protection, such as {@link SessionFixationProtectionEvent}.
*/
private ApplicationEventPublisher applicationEventPublisher = new NullEventPublisher();
/**
* If set to {@code true}, a session will always be created, even if one didn't exist at the start of the request.
* Defaults to {@code false}.
*/
private boolean alwaysCreateSession;
/**
* Called when a user is newly authenticated.
* <p>
* If a session already exists, and matches the session Id from the client, a new session will be created, and the
* session attributes copied to it (if {@code migrateSessionAttributes} is set).
* If the client's requested session Id is invalid, nothing will be done, since there is no need to change the
* session Id if it doesn't match the current session.
* <p>
* If there is no session, no action is taken unless the {@code alwaysCreateSession} property is set, in which
* case a session will be created if one doesn't already exist.
*/
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
boolean hadSessionAlready = request.getSession(false) != null;
if (!hadSessionAlready && !alwaysCreateSession) {
// Session fixation isn't a problem if there's no session
return;
}
// Create new session if necessary
HttpSession session = request.getSession();
if (hadSessionAlready && request.isRequestedSessionIdValid()) {
// We need to migrate to a new session
String originalSessionId = session.getId();
session = applySessionFixation(request);
if (originalSessionId.equals(session.getId())) {
logger.warn("Your servlet container did not change the session ID when a new session was created. You will" +
" not be adequately protected against session-fixation attacks");
}
onSessionChange(originalSessionId, session, authentication);
}
}
/**
* Applies session fixation
*
* @param request the {@link HttpServletRequest} to apply session fixation protection for
* @return the new {@link HttpSession} to use. Cannot be null.
*/
abstract HttpSession applySessionFixation(HttpServletRequest request);
/**
* Called when the session has been changed and the old attributes have been migrated to the new session.
* Only called if a session existed to start with. Allows subclasses to plug in additional behaviour.
* * <p>
* The default implementation of this method publishes a {@link SessionFixationProtectionEvent} to notify
* the application that the session ID has changed. If you override this method and still wish these events to be
* published, you should call {@code super.onSessionChange()} within your overriding method.
*
* @param originalSessionId the original session identifier
* @param newSession the newly created session
* @param auth the token for the newly authenticated principal
*/
protected void onSessionChange(String originalSessionId, HttpSession newSession, Authentication auth) {
applicationEventPublisher.publishEvent(new SessionFixationProtectionEvent(
auth, originalSessionId, newSession.getId()
));
}
/**
* Sets the {@link ApplicationEventPublisher} to use for submitting
* {@link SessionFixationProtectionEvent}. The default is to not submit the
* {@link SessionFixationProtectionEvent}.
*
* @param applicationEventPublisher
* the {@link ApplicationEventPublisher}. Cannot be null.
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
this.applicationEventPublisher = applicationEventPublisher;
}
public void setAlwaysCreateSession(boolean alwaysCreateSession) {
this.alwaysCreateSession = alwaysCreateSession;
}
protected static final class NullEventPublisher implements
ApplicationEventPublisher {
public void publishEvent(ApplicationEvent event) {
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.session;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.util.ReflectionUtils;
/**
* Uses {@link HttpServletRequest#changeSessionId()} to protect against session
* fixation attacks. This is the default implementation for Servlet 3.1+.
*
* @author Rob Winch
* @since 3.2
*/
public final class ChangeSessionIdAuthenticationStrategy extends AbstractSessionFixationProtectionStrategy {
private final Method changeSessionIdMethod;
public ChangeSessionIdAuthenticationStrategy() {
Method changeSessionIdMethod = ReflectionUtils.findMethod(HttpServletRequest.class, "changeSessionId");
if(changeSessionIdMethod == null) {
throw new IllegalStateException("HttpServletRequest.changeSessionId is undefined. Are you using a Servlet 3.1+ environment?");
}
this.changeSessionIdMethod = changeSessionIdMethod;
}
/* (non-Javadoc)
* @see org.springframework.security.web.authentication.session.AbstractSessionFixationProtectionStrategy#applySessionFixation(javax.servlet.http.HttpServletRequest)
*/
@Override
HttpSession applySessionFixation(HttpServletRequest request) {
ReflectionUtils.invokeMethod(changeSessionIdMethod, request);
return request.getSession();
}
}
@@ -22,19 +22,12 @@ import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* The default implementation of {@link SessionAuthenticationStrategy}.
* The default implementation of {@link SessionAuthenticationStrategy} when using < Servlet 3.1.
* <p>
* Creates a new session for the newly authenticated user if they already have a session (as a defence against
* session-fixation protection attacks), and copies their session attributes across to the new session.
@@ -59,19 +52,12 @@ import org.springframework.util.Assert;
* @author Luke Taylor
* @since 3.0
*/
public class SessionFixationProtectionStrategy implements SessionAuthenticationStrategy, ApplicationEventPublisherAware {
protected final Log logger = LogFactory.getLog(this.getClass());
/**
* Used for publishing events related to session fixation protection, such as {@link SessionFixationProtectionEvent}.
*/
private ApplicationEventPublisher applicationEventPublisher = new NullEventPublisher();
public class SessionFixationProtectionStrategy extends AbstractSessionFixationProtectionStrategy {
/**
* Indicates that the session attributes of an existing session
* should be migrated to the new session. Defaults to <code>true</code>.
*/
private boolean migrateSessionAttributes = true;
boolean migrateSessionAttributes = true;
/**
* In the case where the attributes will not be migrated, this field allows a list of named attributes
@@ -79,82 +65,6 @@ public class SessionFixationProtectionStrategy implements SessionAuthenticationS
*/
private List<String> retainedAttributes = null;
/**
* If set to {@code true}, a session will always be created, even if one didn't exist at the start of the request.
* Defaults to {@code false}.
*/
private boolean alwaysCreateSession;
/**
* Called when a user is newly authenticated.
* <p>
* If a session already exists, and matches the session Id from the client, a new session will be created, and the
* session attributes copied to it (if {@code migrateSessionAttributes} is set).
* If the client's requested session Id is invalid, nothing will be done, since there is no need to change the
* session Id if it doesn't match the current session.
* <p>
* If there is no session, no action is taken unless the {@code alwaysCreateSession} property is set, in which
* case a session will be created if one doesn't already exist.
*/
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
boolean hadSessionAlready = request.getSession(false) != null;
if (!hadSessionAlready && !alwaysCreateSession) {
// Session fixation isn't a problem if there's no session
return;
}
// Create new session if necessary
HttpSession session = request.getSession();
if (hadSessionAlready && request.isRequestedSessionIdValid()) {
// We need to migrate to a new session
String originalSessionId = session.getId();
if (logger.isDebugEnabled()) {
logger.debug("Invalidating session with Id '" + originalSessionId +"' " + (migrateSessionAttributes ?
"and" : "without") + " migrating attributes.");
}
Map<String, Object> attributesToMigrate = extractAttributes(session);
session.invalidate();
session = request.getSession(true); // we now have a new session
if (logger.isDebugEnabled()) {
logger.debug("Started new session: " + session.getId());
}
if (originalSessionId.equals(session.getId())) {
logger.warn("Your servlet container did not change the session ID when a new session was created. You will" +
" not be adequately protected against session-fixation attacks");
}
transferAttributes(attributesToMigrate, session);
onSessionChange(originalSessionId, session, authentication);
}
}
/**
* Called when the session has been changed and the old attributes have been migrated to the new session.
* Only called if a session existed to start with. Allows subclasses to plug in additional behaviour.
* * <p>
* The default implementation of this method publishes a {@link SessionFixationProtectionEvent} to notify
* the application that the session ID has changed. If you override this method and still wish these events to be
* published, you should call {@code super.onSessionChange()} within your overriding method.
*
* @param originalSessionId the original session identifier
* @param newSession the newly created session
* @param auth the token for the newly authenticated principal
*/
protected void onSessionChange(String originalSessionId, HttpSession newSession, Authentication auth) {
applicationEventPublisher.publishEvent(new SessionFixationProtectionEvent(
auth, originalSessionId, newSession.getId()
));
}
/**
* Called to extract the existing attributes from the session, prior to invalidating it. If
* {@code migrateAttributes} is set to {@code false}, only Spring Security attributes will be retained.
@@ -169,11 +79,33 @@ public class SessionFixationProtectionStrategy implements SessionAuthenticationS
return createMigratedAttributeMap(session);
}
@Override
final HttpSession applySessionFixation(HttpServletRequest request) {
HttpSession session = request.getSession();
String originalSessionId = session.getId();
if (logger.isDebugEnabled()) {
logger.debug("Invalidating session with Id '" + originalSessionId +"' " + (migrateSessionAttributes ?
"and" : "without") + " migrating attributes.");
}
Map<String, Object> attributesToMigrate = extractAttributes(session);
session.invalidate();
session = request.getSession(true); // we now have a new session
if (logger.isDebugEnabled()) {
logger.debug("Started new session: " + session.getId());
}
transferAttributes(attributesToMigrate, session);
return session;
}
/**
* @param attributes the attributes which were extracted from the original session by {@code extractAttributes}
* @param newSession the newly created session
*/
private void transferAttributes(Map<String, Object> attributes, HttpSession newSession) {
void transferAttributes(Map<String, Object> attributes, HttpSession newSession) {
if (attributes != null) {
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
newSession.setAttribute(entry.getKey(), entry.getValue());
@@ -214,19 +146,6 @@ public class SessionFixationProtectionStrategy implements SessionAuthenticationS
return attributesToMigrate;
}
/**
* Sets the {@link ApplicationEventPublisher} to use for submitting
* {@link SessionFixationProtectionEvent}. The default is to not submit the
* {@link SessionFixationProtectionEvent}.
*
* @param applicationEventPublisher
* the {@link ApplicationEventPublisher}. Cannot be null.
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Defines whether attributes should be migrated to a new session or not. Has no effect if you
* override the {@code extractAttributes} method.
@@ -250,12 +169,4 @@ public class SessionFixationProtectionStrategy implements SessionAuthenticationS
Assert.notNull(retainedAttributes);
this.retainedAttributes = retainedAttributes;
}
public void setAlwaysCreateSession(boolean alwaysCreateSession) {
this.alwaysCreateSession = alwaysCreateSession;
}
private static final class NullEventPublisher implements ApplicationEventPublisher {
public void publishEvent(ApplicationEvent event) { }
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.session;
import static org.mockito.Matchers.*;
import static org.powermock.api.mockito.PowerMockito.*;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.ReflectionUtils;
/**
* @author Rob Winch
*
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({ReflectionUtils.class, Method.class})
public class ChangeSessionIdAuthenticationStrategyTests {
@Mock
private Method method;
@Test(expected = IllegalStateException.class)
public void constructChangeIdMethodNotFound() {
new ChangeSessionIdAuthenticationStrategy();
}
@Test
public void applySessionFixation() throws Exception {
spy(ReflectionUtils.class);
Method method = mock(Method.class);
MockHttpServletRequest request = new MockHttpServletRequest();
request.getSession();
when(ReflectionUtils.findMethod(HttpServletRequest.class, "changeSessionId")).thenReturn(method);
new ChangeSessionIdAuthenticationStrategy().applySessionFixation(request);
verifyStatic();
ReflectionUtils.invokeMethod(same(method), eq(request));
}
}