From 7f714ebb230d397750f7dd91761c43702f41a8b3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 11 Dec 2013 17:38:17 -0600 Subject: [PATCH] SEC-2422: Session timeout detection with CSRF protection --- .../web/configurers/CsrfConfigurer.java | 84 +++++++++++++++++-- .../SessionManagementConfigurer.java | 23 ++++- .../config/http/CsrfBeanDefinitionParser.java | 59 ++++++++++++- .../config/http/HttpConfigurationBuilder.java | 21 +++-- .../configurers/CsrfConfigurerTests.groovy | 44 ++++++++-- .../config/http/CsrfConfigTests.groovy | 22 +++++ .../access/DelegatingAccessDeniedHandler.java | 81 ++++++++++++++++++ .../security/web/csrf/CsrfException.java | 32 +++++++ .../security/web/csrf/CsrfFilter.java | 9 +- .../web/csrf/InvalidCsrfTokenException.java | 11 ++- .../web/csrf/MissingCsrfTokenException.java | 30 +++++++ .../InvalidSessionAccessDeniedHandler.java | 53 ++++++++++++ .../DelegatingAccessDeniedHandlerTests.java | 84 +++++++++++++++++++ 13 files changed, 523 insertions(+), 30 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/access/DelegatingAccessDeniedHandler.java create mode 100644 web/src/main/java/org/springframework/security/web/csrf/CsrfException.java create mode 100644 web/src/main/java/org/springframework/security/web/csrf/MissingCsrfTokenException.java create mode 100644 web/src/main/java/org/springframework/security/web/session/InvalidSessionAccessDeniedHandler.java create mode 100644 web/src/test/java/org/springframework/security/web/access/DelegatingAccessDeniedHandlerTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java index 993c8c420d..2ed4d28dc7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -15,14 +15,22 @@ */ package org.springframework.security.config.annotation.web.configurers; +import java.util.LinkedHashMap; + +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.DelegatingAccessDeniedHandler; import org.springframework.security.web.csrf.CsrfAuthenticationStrategy; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.csrf.CsrfLogoutHandler; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import org.springframework.security.web.csrf.MissingCsrfTokenException; +import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler; +import org.springframework.security.web.session.InvalidSessionStrategy; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -50,6 +58,7 @@ import org.springframework.util.Assert; *
  • * {@link ExceptionHandlingConfigurer#accessDeniedHandler(AccessDeniedHandler)} * is used to determine how to handle CSRF attempts
  • + *
  • {@link InvalidSessionStrategy}
  • * * * @author Rob Winch @@ -100,12 +109,9 @@ public final class CsrfConfigurer> extends Abst if(requireCsrfProtectionMatcher != null) { filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher); } - ExceptionHandlingConfigurer exceptionConfig = http.getConfigurer(ExceptionHandlingConfigurer.class); - if(exceptionConfig != null) { - AccessDeniedHandler accessDeniedHandler = exceptionConfig.getAccessDeniedHandler(); - if(accessDeniedHandler != null) { - filter.setAccessDeniedHandler(accessDeniedHandler); - } + AccessDeniedHandler accessDeniedHandler = createAccessDeniedHandler(http); + if(accessDeniedHandler != null) { + filter.setAccessDeniedHandler(accessDeniedHandler); } LogoutConfigurer logoutConfigurer = http.getConfigurer(LogoutConfigurer.class); if(logoutConfigurer != null) { @@ -118,4 +124,70 @@ public final class CsrfConfigurer> extends Abst filter = postProcess(filter); http.addFilter(filter); } + + /** + * Gets the default {@link AccessDeniedHandler} from the + * {@link ExceptionHandlingConfigurer#getAccessDeniedHandler()} or create a + * {@link AccessDeniedHandlerImpl} if not available. + * + * @param http the {@link HttpSecurityBuilder} + * @return the {@link AccessDeniedHandler} + */ + @SuppressWarnings("unchecked") + private AccessDeniedHandler getDefaultAccessDeniedHandler(H http) { + ExceptionHandlingConfigurer exceptionConfig = http.getConfigurer(ExceptionHandlingConfigurer.class); + AccessDeniedHandler handler = null; + if(exceptionConfig != null) { + handler = exceptionConfig.getAccessDeniedHandler(); + } + if(handler == null) { + handler = new AccessDeniedHandlerImpl(); + } + return handler; + } + + /** + * Gets the default {@link InvalidSessionStrategy} from the + * {@link SessionManagementConfigurer#getInvalidSessionStrategy()} or null + * if not available. + * + * @param http + * the {@link HttpSecurityBuilder} + * @return the {@link InvalidSessionStrategy} + */ + @SuppressWarnings("unchecked") + private InvalidSessionStrategy getInvalidSessionStrategy(H http) { + SessionManagementConfigurer sessionManagement = http.getConfigurer(SessionManagementConfigurer.class); + if(sessionManagement == null) { + return null; + } + return sessionManagement.getInvalidSessionStrategy(); + } + + /** + * Creates the {@link AccessDeniedHandler} from the result of + * {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)} and + * {@link #getInvalidSessionStrategy(HttpSecurityBuilder)}. If + * {@link #getInvalidSessionStrategy(HttpSecurityBuilder)} is non-null, then + * a {@link DelegatingAccessDeniedHandler} is used in combination with + * {@link InvalidSessionAccessDeniedHandler} and the + * {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)}. Otherwise, + * only {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)} is used. + * + * @param http the {@link HttpSecurityBuilder} + * @return the {@link AccessDeniedHandler} + */ + private AccessDeniedHandler createAccessDeniedHandler(H http) { + InvalidSessionStrategy invalidSessionStrategy = getInvalidSessionStrategy(http); + AccessDeniedHandler defaultAccessDeniedHandler = getDefaultAccessDeniedHandler(http); + if(invalidSessionStrategy == null) { + return defaultAccessDeniedHandler; + } + + InvalidSessionAccessDeniedHandler invalidSessionDeniedHandler = new InvalidSessionAccessDeniedHandler(invalidSessionStrategy); + LinkedHashMap, AccessDeniedHandler> handlers = + new LinkedHashMap, AccessDeniedHandler>(); + handlers.put(MissingCsrfTokenException.class, invalidSessionDeniedHandler); + return new DelegatingAccessDeniedHandler(handlers, defaultAccessDeniedHandler); + } } \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 6328fcf631..78d2058d23 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -42,6 +42,7 @@ import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.InvalidSessionStrategy; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; import org.springframework.util.Assert; @@ -66,6 +67,7 @@ import org.springframework.util.Assert; *
  • {@link RequestCache}
  • *
  • {@link SecurityContextRepository}
  • *
  • {@link SessionManagementConfigurer}
  • + *
  • {@link InvalidSessionStrategy}
  • * * *

    Shared Objects Used

    @@ -83,6 +85,7 @@ import org.springframework.util.Assert; public final class SessionManagementConfigurer> extends AbstractHttpConfigurer,H> { private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = createDefaultSessionFixationProtectionStrategy(); private SessionAuthenticationStrategy sessionAuthenticationStrategy; + private InvalidSessionStrategy invalidSessionStrategy; private List sessionAuthenticationStrategies = new ArrayList(); private SessionRegistry sessionRegistry = new SessionRegistryImpl(); private Integer maximumSessions; @@ -365,6 +368,7 @@ public final class SessionManagementConfigurer> } } http.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy()); + http.setSharedObject(InvalidSessionStrategy.class, getInvalidSessionStrategy()); } @Override @@ -375,7 +379,7 @@ public final class SessionManagementConfigurer> sessionManagementFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(sessionAuthenticationErrorUrl)); } if(invalidSessionUrl != null) { - sessionManagementFilter.setInvalidSessionStrategy(new SimpleRedirectInvalidSessionStrategy(invalidSessionUrl)); + sessionManagementFilter.setInvalidSessionStrategy(getInvalidSessionStrategy()); } AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class); if(trustResolver != null) { @@ -391,6 +395,23 @@ public final class SessionManagementConfigurer> } } + /** + * Gets the {@link InvalidSessionStrategy} to use. If + * {@link #invalidSessionUrl} is null, returns null otherwise + * {@link SimpleRedirectInvalidSessionStrategy} is used. + * + * @return the {@link InvalidSessionStrategy} to use + */ + InvalidSessionStrategy getInvalidSessionStrategy() { + if(invalidSessionUrl == null) { + return null; + } + if(invalidSessionStrategy == null) { + invalidSessionStrategy = new SimpleRedirectInvalidSessionStrategy(invalidSessionUrl); + } + return invalidSessionStrategy; + } + /** * Gets the {@link SessionCreationPolicy}. Can not be null. * @return the {@link SessionCreationPolicy} diff --git a/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java index 2fa1b9d9fa..2b5aa6282b 100644 --- a/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java @@ -15,17 +15,26 @@ */ package org.springframework.security.config.http; +import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.DelegatingAccessDeniedHandler; import org.springframework.security.web.csrf.CsrfAuthenticationStrategy; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.csrf.CsrfLogoutHandler; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import org.springframework.security.web.csrf.MissingCsrfTokenException; import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; +import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler; +import org.springframework.security.web.session.InvalidSessionStrategy; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.w3c.dom.Element; @@ -44,6 +53,7 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser { private static final String ATT_REPOSITORY = "token-repository-ref"; private String csrfRepositoryRef; + private BeanDefinition csrfFilter; public BeanDefinition parse(Element element, ParserContext pc) { boolean webmvcPresent = ClassUtils.isPresent(DISPATCHER_SERVLET_CLASS_NAME, getClass().getClassLoader()); @@ -58,21 +68,64 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser { csrfRepositoryRef = element.getAttribute(ATT_REPOSITORY); String matcherRef = element.getAttribute(ATT_MATCHER); - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(CsrfFilter.class); - if(!StringUtils.hasText(csrfRepositoryRef)) { RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class); csrfRepositoryRef = pc.getReaderContext().generateBeanName(csrfTokenRepository); pc.registerBeanComponent(new BeanComponentDefinition(csrfTokenRepository, csrfRepositoryRef)); } + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(CsrfFilter.class); builder.addConstructorArgReference(csrfRepositoryRef); if(StringUtils.hasText(matcherRef)) { builder.addPropertyReference("requireCsrfProtectionMatcher", matcherRef); } - return builder.getBeanDefinition(); + csrfFilter = builder.getBeanDefinition(); + return csrfFilter; + } + + /** + * Populate the AccessDeniedHandler on the {@link CsrfFilter} + * + * @param invalidSessionStrategy the {@link InvalidSessionStrategy} to use + * @param defaultDeniedHandler the {@link AccessDeniedHandler} to use + */ + void initAccessDeniedHandler(BeanDefinition invalidSessionStrategy, BeanMetadataElement defaultDeniedHandler) { + BeanMetadataElement accessDeniedHandler = createAccessDeniedHandler(invalidSessionStrategy, defaultDeniedHandler); + csrfFilter.getPropertyValues().addPropertyValue("accessDeniedHandler", accessDeniedHandler); + } + + /** + * Creates the {@link AccessDeniedHandler} from the result of + * {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)} and + * {@link #getInvalidSessionStrategy(HttpSecurityBuilder)}. If + * {@link #getInvalidSessionStrategy(HttpSecurityBuilder)} is non-null, then + * a {@link DelegatingAccessDeniedHandler} is used in combination with + * {@link InvalidSessionAccessDeniedHandler} and the + * {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)}. Otherwise, + * only {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)} is used. + * + * @param invalidSessionStrategy the {@link InvalidSessionStrategy} to use + * @param defaultDeniedHandler the {@link AccessDeniedHandler} to use + * + * @return the {@link BeanMetadataElement} that is the {@link AccessDeniedHandler} to populate on the {@link CsrfFilter} + */ + private BeanMetadataElement createAccessDeniedHandler(BeanDefinition invalidSessionStrategy, BeanMetadataElement defaultDeniedHandler) { + if(invalidSessionStrategy == null) { + return defaultDeniedHandler; + } + ManagedMap,BeanDefinition> handlers = + new ManagedMap, BeanDefinition>(); + BeanDefinitionBuilder invalidSessionHandlerBldr = BeanDefinitionBuilder.rootBeanDefinition(InvalidSessionAccessDeniedHandler.class); + invalidSessionHandlerBldr.addConstructorArgValue(invalidSessionStrategy); + handlers.put(MissingCsrfTokenException.class, invalidSessionHandlerBldr.getBeanDefinition()); + + BeanDefinitionBuilder deniedBldr = BeanDefinitionBuilder.rootBeanDefinition(DelegatingAccessDeniedHandler.class); + deniedBldr.addConstructorArgValue(handlers); + deniedBldr.addConstructorArgValue(defaultDeniedHandler); + + return deniedBldr.getBeanDefinition(); } BeanDefinition getCsrfAuthenticationStrategy() { diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index f6f789e74b..c4ff37ffd8 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -130,6 +130,10 @@ class HttpConfigurationBuilder { private BeanMetadataElement csrfLogoutHandler; private BeanMetadataElement csrfAuthStrategy; + private CsrfBeanDefinitionParser csrfParser; + + private BeanDefinition invalidSession; + public HttpConfigurationBuilder(Element element, ParserContext pc, BeanReference portMapper, BeanReference portResolver, BeanReference authenticationManager) { this.httpElt = element; @@ -200,8 +204,8 @@ class HttpConfigurationBuilder { } void setAccessDeniedHandler(BeanMetadataElement accessDeniedHandler) { - if(csrfFilter != null) { - csrfFilter.getPropertyValues().add("accessDeniedHandler", accessDeniedHandler); + if(csrfParser != null ) { + csrfParser.initAccessDeniedHandler(this.invalidSession, accessDeniedHandler); } } @@ -381,7 +385,10 @@ class HttpConfigurationBuilder { } if (StringUtils.hasText(invalidSessionUrl)) { - sessionMgmtFilter.addPropertyValue("invalidSessionStrategy", new SimpleRedirectInvalidSessionStrategy(invalidSessionUrl)); + BeanDefinitionBuilder invalidSessionBldr = BeanDefinitionBuilder.rootBeanDefinition(SimpleRedirectInvalidSessionStrategy.class); + invalidSessionBldr.addConstructorArgValue(invalidSessionUrl); + invalidSession = invalidSessionBldr.getBeanDefinition(); + sessionMgmtFilter.addPropertyValue("invalidSessionStrategy", invalidSession); } sessionMgmtFilter.addConstructorArgReference(sessionAuthStratRef); @@ -637,14 +644,16 @@ class HttpConfigurationBuilder { } - private void createCsrfFilter() { + private CsrfBeanDefinitionParser createCsrfFilter() { Element elmt = DomUtils.getChildElementByTagName(httpElt, Elements.CSRF); if (elmt != null) { - CsrfBeanDefinitionParser csrfParser = new CsrfBeanDefinitionParser(); - this.csrfFilter = csrfParser.parse(elmt, pc); + csrfParser = new CsrfBeanDefinitionParser(); + csrfFilter = csrfParser.parse(elmt, pc); this.csrfAuthStrategy = csrfParser.getCsrfAuthenticationStrategy(); this.csrfLogoutHandler = csrfParser.getCsrfLogoutHandler(); + return csrfParser; } + return null; } BeanMetadataElement getCsrfLogoutHandler() { diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy index 564d3fabd1..8aba928683 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy @@ -18,19 +18,20 @@ package org.springframework.security.config.annotation.web.configurers import javax.servlet.http.HttpServletResponse import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse import org.springframework.security.config.annotation.BaseSpringSpec import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.web.access.AccessDeniedHandler -import org.springframework.security.web.csrf.CsrfFilter; -import org.springframework.security.web.csrf.CsrfTokenRepository; -import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.web.servlet.support.RequestDataValueProcessor; +import org.springframework.security.web.csrf.CsrfFilter +import org.springframework.security.web.csrf.CsrfTokenRepository +import org.springframework.security.web.util.matcher.RequestMatcher +import org.springframework.web.servlet.support.RequestDataValueProcessor -import spock.lang.Unroll; +import spock.lang.Unroll /** * @@ -100,6 +101,37 @@ class CsrfConfigurerTests extends BaseSpringSpec { } } + def "SEC-2422: csrf expire CSRF token and session-management invalid-session-url"() { + setup: + loadConfig(InvalidSessionUrlConfig) + request.session.clearAttributes() + request.setParameter("_csrf","abc") + request.method = "POST" + when: "No existing expected CsrfToken (session times out) and a POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to the session timeout page page" + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == "/error/sessionError" + when: "Existing expected CsrfToken and a POST (invalid token provided)" + response = new MockHttpServletResponse() + request = new MockHttpServletRequest(session: request.session, method:'POST') + springSecurityFilterChain.doFilter(request,response,chain) + then: "Access Denied occurs" + response.status == HttpServletResponse.SC_FORBIDDEN + } + + @Configuration + @EnableWebSecurity + static class InvalidSessionUrlConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().and() + .sessionManagement() + .invalidSessionUrl("/error/sessionError") + } + } + def "csrf requireCsrfProtectionMatcher"() { setup: RequireCsrfProtectionMatcherConfig.matcher = Mock(RequestMatcher) diff --git a/config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy index 5360de28d8..14cd0a74fe 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy @@ -174,6 +174,28 @@ class CsrfConfigTests extends AbstractHttpConfigTests { response.redirectedUrl == "http://localhost/some-url" } + def "SEC-2422: csrf expire CSRF token and session-management invalid-session-url"() { + setup: + httpAutoConfig { + 'csrf'() + 'session-management'('invalid-session-url': '/error/sessionError') + } + createAppContext() + request.setParameter("_csrf","abc") + request.method = "POST" + when: "No existing expected CsrfToken (session times out) and a POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to the session timeout page page" + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == "/error/sessionError" + when: "Existing expected CsrfToken and a POST (invalid token provided)" + response = new MockHttpServletResponse() + request = new MockHttpServletRequest(session: request.session, method:'POST') + springSecurityFilterChain.doFilter(request,response,chain) + then: "Access Denied occurs" + response.status == HttpServletResponse.SC_FORBIDDEN + } + def "csrf requireCsrfProtectionMatcher"() { setup: httpAutoConfig { diff --git a/web/src/main/java/org/springframework/security/web/access/DelegatingAccessDeniedHandler.java b/web/src/main/java/org/springframework/security/web/access/DelegatingAccessDeniedHandler.java new file mode 100644 index 0000000000..992c874da0 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/DelegatingAccessDeniedHandler.java @@ -0,0 +1,81 @@ +/* + * 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.access; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map.Entry; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; + +/** + * An {@link AccessDeniedHandler} that delegates to other + * {@link AccessDeniedHandler} instances based upon the type of + * {@link AccessDeniedException} passed into + * {@link #handle(HttpServletRequest, HttpServletResponse, AccessDeniedException)}. + * + * @author Rob Winch + * @since 3.2 + * + */ +public final class DelegatingAccessDeniedHandler implements AccessDeniedHandler { + private final LinkedHashMap, AccessDeniedHandler> handlers; + + private final AccessDeniedHandler defaultHander; + + /** + * Creates a new instance + * + * @param handlers + * a map of the {@link AccessDeniedException} class to the + * {@link AccessDeniedHandler} that should be used. Each is + * considered in the order they are specified and only the first + * {@link AccessDeniedHandler} is ued. + * @param defaultHander + * the default {@link AccessDeniedHandler} that should be used if + * none of the handlers matches. + */ + public DelegatingAccessDeniedHandler( + LinkedHashMap, AccessDeniedHandler> handlers, + AccessDeniedHandler defaultHander) { + Assert.notEmpty(handlers, "handlers cannot be null or empty"); + Assert.notNull(defaultHander, "defaultHandler cannot be null"); + this.handlers = handlers; + this.defaultHander = defaultHander; + } + + + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, + ServletException { + for(Entry, AccessDeniedHandler> entry : handlers.entrySet()) { + Class handlerClass = entry.getKey(); + if(handlerClass.isAssignableFrom(accessDeniedException.getClass())) { + AccessDeniedHandler handler = entry.getValue(); + handler.handle(request, response, accessDeniedException); + return; + } + } + defaultHander.handle(request, response, accessDeniedException); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfException.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfException.java new file mode 100644 index 0000000000..f21c50c208 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfException.java @@ -0,0 +1,32 @@ +/* + * 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.csrf; + +import org.springframework.security.access.AccessDeniedException; + +/** + * Thrown when an invalid or missing {@link CsrfToken} is found in the HttpServletRequest + * + * @author Rob Winch + * @since 3.2 + */ +@SuppressWarnings("serial") +public class CsrfException extends AccessDeniedException { + + public CsrfException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java index 6c5d74517a..683d20d4ab 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java @@ -73,7 +73,8 @@ public final class CsrfFilter extends OncePerRequestFilter { HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { CsrfToken csrfToken = tokenRepository.loadToken(request); - if(csrfToken == null) { + final boolean missingToken = csrfToken == null; + if(missingToken) { CsrfToken generatedToken = tokenRepository.generateToken(request); csrfToken = new SaveOnAccessCsrfToken(tokenRepository, request, response, generatedToken); } @@ -93,7 +94,11 @@ public final class CsrfFilter extends OncePerRequestFilter { if(logger.isDebugEnabled()) { logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)); } - accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken)); + if(missingToken) { + accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken)); + } else { + accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken)); + } return; } diff --git a/web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java b/web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java index 38faff7e35..3b8fc0ef0d 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java +++ b/web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java @@ -15,17 +15,17 @@ */ package org.springframework.security.web.csrf; -import org.springframework.security.access.AccessDeniedException; - +import javax.servlet.http.HttpServletRequest; /** - * Thrown when an invalid or missing {@link CsrfToken} is found in the HttpServletRequest + * Thrown when an expected {@link CsrfToken} exists, but it does not match the + * value present on the {@link HttpServletRequest} * * @author Rob Winch * @since 3.2 */ @SuppressWarnings("serial") -public class InvalidCsrfTokenException extends AccessDeniedException { +public class InvalidCsrfTokenException extends CsrfException { /** * @param msg @@ -36,5 +36,4 @@ public class InvalidCsrfTokenException extends AccessDeniedException { + expectedAccessToken.getParameterName() + "' or header '" + expectedAccessToken.getHeaderName() + "'."); } - -} +} \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/csrf/MissingCsrfTokenException.java b/web/src/main/java/org/springframework/security/web/csrf/MissingCsrfTokenException.java new file mode 100644 index 0000000000..855a0201de --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/MissingCsrfTokenException.java @@ -0,0 +1,30 @@ +/* + * 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.csrf; + +/** + * Thrown when no expected {@link CsrfToken} is found but is required. + * + * @author Rob Winch + * @since 3.2 + */ +@SuppressWarnings("serial") +public class MissingCsrfTokenException extends CsrfException { + + public MissingCsrfTokenException(String actualToken) { + super("Expected CSRF token not found. Has your session expired?"); + } +} \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/session/InvalidSessionAccessDeniedHandler.java b/web/src/main/java/org/springframework/security/web/session/InvalidSessionAccessDeniedHandler.java new file mode 100644 index 0000000000..b8f7b1d82b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/session/InvalidSessionAccessDeniedHandler.java @@ -0,0 +1,53 @@ +/* + * 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.session; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.util.Assert; + +/** + * An adapter of {@link InvalidSessionStrategy} to {@link AccessDeniedHandler} + * + * @author Rob Winch + * @since 3.2 + */ +public final class InvalidSessionAccessDeniedHandler implements AccessDeniedHandler { + private final InvalidSessionStrategy invalidSessionStrategy; + + /** + * Creates a new instance + * @param invalidSessionStrategy the {@link InvalidSessionStrategy} to delegate to + */ + public InvalidSessionAccessDeniedHandler( + InvalidSessionStrategy invalidSessionStrategy) { + Assert.notNull(invalidSessionStrategy, "invalidSessionStrategy cannot be null"); + this.invalidSessionStrategy = invalidSessionStrategy; + } + + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, + ServletException { + invalidSessionStrategy.onInvalidSessionDetected(request, response); + } +} \ No newline at end of file diff --git a/web/src/test/java/org/springframework/security/web/access/DelegatingAccessDeniedHandlerTests.java b/web/src/test/java/org/springframework/security/web/access/DelegatingAccessDeniedHandlerTests.java new file mode 100644 index 0000000000..6d8e59bb63 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/DelegatingAccessDeniedHandlerTests.java @@ -0,0 +1,84 @@ +/* + * 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.access; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.LinkedHashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.csrf.CsrfException; +import org.springframework.security.web.csrf.InvalidCsrfTokenException; +import org.springframework.security.web.csrf.MissingCsrfTokenException; + +@RunWith(MockitoJUnitRunner.class) +public class DelegatingAccessDeniedHandlerTests { + @Mock + private AccessDeniedHandler handler1; + @Mock + private AccessDeniedHandler handler2; + @Mock + private AccessDeniedHandler handler3; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + + private LinkedHashMap,AccessDeniedHandler> handlers; + + private DelegatingAccessDeniedHandler handler; + + @Before + public void setup() { + handlers = new LinkedHashMap, AccessDeniedHandler>(); + } + + @Test + public void moreSpecificDoesNotInvokeLessSpecific() throws Exception { + handlers.put(CsrfException.class, handler1); + handler = new DelegatingAccessDeniedHandler(handlers, handler3); + + AccessDeniedException accessDeniedException = new AccessDeniedException(""); + handler.handle(request, response, accessDeniedException); + + verify(handler1,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class)); + verify(handler3).handle(request, response, accessDeniedException); + } + + @Test + public void matchesDoesNotInvokeDefault() throws Exception { + handlers.put(InvalidCsrfTokenException.class, handler1); + handlers.put(MissingCsrfTokenException.class, handler2); + handler = new DelegatingAccessDeniedHandler(handlers, handler3); + + AccessDeniedException accessDeniedException = new MissingCsrfTokenException("123"); + handler.handle(request, response, accessDeniedException); + + verify(handler1,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class)); + verify(handler2).handle(request, response, accessDeniedException); + verify(handler3,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class)); + } +}