SEC-2347: CSRF Enabled by default w/ XML Config

This commit is contained in:
Rob Winch 2014-11-21 21:32:56 -06:00
parent eedbf44235
commit 4392205f63
15 changed files with 93 additions and 39 deletions

View File

@ -56,6 +56,10 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
private BeanDefinition csrfFilter; private BeanDefinition csrfFilter;
public BeanDefinition parse(Element element, ParserContext pc) { public BeanDefinition parse(Element element, ParserContext pc) {
boolean disabled = element != null && "true".equals(element.getAttribute("disabled"));
if(disabled) {
return null;
}
boolean webmvcPresent = ClassUtils.isPresent(DISPATCHER_SERVLET_CLASS_NAME, getClass().getClassLoader()); boolean webmvcPresent = ClassUtils.isPresent(DISPATCHER_SERVLET_CLASS_NAME, getClass().getClassLoader());
if(webmvcPresent) { if(webmvcPresent) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(CsrfRequestDataValueProcessor.class); RootBeanDefinition beanDefinition = new RootBeanDefinition(CsrfRequestDataValueProcessor.class);
@ -64,8 +68,11 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
pc.registerBeanComponent(componentDefinition); pc.registerBeanComponent(componentDefinition);
} }
csrfRepositoryRef = element.getAttribute(ATT_REPOSITORY); String matcherRef = null;
String matcherRef = element.getAttribute(ATT_MATCHER); if(element != null) {
csrfRepositoryRef = element.getAttribute(ATT_REPOSITORY);
matcherRef = element.getAttribute(ATT_MATCHER);
}
if(!StringUtils.hasText(csrfRepositoryRef)) { if(!StringUtils.hasText(csrfRepositoryRef)) {
RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class); RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class);

View File

@ -642,16 +642,18 @@ class HttpConfigurationBuilder {
} }
private CsrfBeanDefinitionParser createCsrfFilter() { private void createCsrfFilter() {
Element elmt = DomUtils.getChildElementByTagName(httpElt, Elements.CSRF); Element elmt = DomUtils.getChildElementByTagName(httpElt, Elements.CSRF);
if (elmt != null) { csrfParser = new CsrfBeanDefinitionParser();
csrfParser = new CsrfBeanDefinitionParser(); csrfFilter = csrfParser.parse(elmt, pc);
csrfFilter = csrfParser.parse(elmt, pc);
this.csrfAuthStrategy = csrfParser.getCsrfAuthenticationStrategy(); if(csrfFilter == null) {
this.csrfLogoutHandler = csrfParser.getCsrfLogoutHandler(); csrfParser = null;
return csrfParser; return;
} }
return null;
this.csrfAuthStrategy = csrfParser.getCsrfAuthenticationStrategy();
this.csrfLogoutHandler = csrfParser.getCsrfLogoutHandler();
} }
BeanMetadataElement getCsrfLogoutHandler() { BeanMetadataElement getCsrfLogoutHandler() {

View File

@ -721,6 +721,9 @@ jdbc-user-service.attlist &=
csrf = csrf =
## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. ## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests.
element csrf {csrf-options.attlist} element csrf {csrf-options.attlist}
csrf-options.attlist &=
## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled).
attribute disabled {xsd:boolean}?
csrf-options.attlist &= csrf-options.attlist &=
## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS"
attribute request-matcher-ref { xsd:token }? attribute request-matcher-ref { xsd:token }?

View File

@ -2251,6 +2251,13 @@
</xs:complexType> </xs:complexType>
</xs:element> </xs:element>
<xs:attributeGroup name="csrf-options.attlist"> <xs:attributeGroup name="csrf-options.attlist">
<xs:attribute name="disabled" type="xs:boolean">
<xs:annotation>
<xs:documentation>Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is
enabled).
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="request-matcher-ref" type="xs:token"> <xs:attribute name="request-matcher-ref" type="xs:token">
<xs:annotation> <xs:annotation>
<xs:documentation>The RequestMatcher instance to be used to determine if CSRF should be applied. Default is <xs:documentation>The RequestMatcher instance to be used to determine if CSRF should be applied. Default is

View File

@ -26,7 +26,7 @@ import org.springframework.security.web.FilterInvocation
* *
*/ */
abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests { abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests {
final int AUTO_CONFIG_FILTERS = 13; final int AUTO_CONFIG_FILTERS = 14;
def httpAutoConfig(Closure c) { def httpAutoConfig(Closure c) {
xml.http('auto-config': 'true', c) xml.http('auto-config': 'true', c)

View File

@ -47,9 +47,34 @@ class CsrfConfigTests extends AbstractHttpConfigTests {
MockHttpServletResponse response = new MockHttpServletResponse() MockHttpServletResponse response = new MockHttpServletResponse()
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
def 'no http csrf filter by default'() { @Unroll
def 'csrf is enabled by default'() {
setup:
httpAutoConfig {
}
createAppContext()
when:
request.method = httpMethod
springSecurityFilterChain.doFilter(request,response,chain)
then:
response.status == httpStatus
where:
httpMethod | httpStatus
'POST' | HttpServletResponse.SC_FORBIDDEN
'PUT' | HttpServletResponse.SC_FORBIDDEN
'PATCH' | HttpServletResponse.SC_FORBIDDEN
'DELETE' | HttpServletResponse.SC_FORBIDDEN
'INVALID' | HttpServletResponse.SC_FORBIDDEN
'GET' | HttpServletResponse.SC_OK
'HEAD' | HttpServletResponse.SC_OK
'TRACE' | HttpServletResponse.SC_OK
'OPTIONS' | HttpServletResponse.SC_OK
}
def 'csrf disabled'() {
when: when:
httpAutoConfig { httpAutoConfig {
csrf(disabled:true)
} }
createAppContext() createAppContext()
then: then:

View File

@ -16,6 +16,7 @@ class FormLoginBeanDefinitionParserTests extends AbstractHttpConfigTests {
MockHttpServletResponse response = new MockHttpServletResponse() MockHttpServletResponse response = new MockHttpServletResponse()
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
httpAutoConfig { httpAutoConfig {
csrf(disabled:true)
} }
createAppContext() createAppContext()
when: when:
@ -38,6 +39,7 @@ class FormLoginBeanDefinitionParserTests extends AbstractHttpConfigTests {
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
httpAutoConfig { httpAutoConfig {
'form-login'('login-processing-url':'/login_custom','username-parameter':'custom_user','password-parameter':'custom_password') 'form-login'('login-processing-url':'/login_custom','username-parameter':'custom_user','password-parameter':'custom_password')
csrf(disabled:true)
} }
createAppContext() createAppContext()
when: when:
@ -60,6 +62,7 @@ class FormLoginBeanDefinitionParserTests extends AbstractHttpConfigTests {
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
httpAutoConfig { httpAutoConfig {
'openid-login'() 'openid-login'()
csrf(disabled:true)
} }
createAppContext() createAppContext()
when: when:
@ -87,6 +90,7 @@ class FormLoginBeanDefinitionParserTests extends AbstractHttpConfigTests {
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
httpAutoConfig { httpAutoConfig {
'openid-login'('login-processing-url':'/login_custom') 'openid-login'('login-processing-url':'/login_custom')
csrf(disabled:true)
} }
createAppContext() createAppContext()
when: when:

View File

@ -110,6 +110,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
xml.http() { xml.http() {
'http-basic'() 'http-basic'()
'intercept-url'(pattern: '/**', 'method':'PATCH',access: 'ROLE_ADMIN') 'intercept-url'(pattern: '/**', 'method':'PATCH',access: 'ROLE_ADMIN')
csrf(disabled:true)
} }
createAppContext() createAppContext()
when: 'Method other than PATCH is used' when: 'Method other than PATCH is used'

View File

@ -13,6 +13,7 @@ class LogoutConfigTests extends AbstractHttpConfigTests {
when: when:
httpAutoConfig { httpAutoConfig {
'logout'('logout-url':'/logout') 'logout'('logout-url':'/logout')
csrf(disabled:true)
} }
createAppContext() createAppContext()

View File

@ -15,6 +15,7 @@
*/ */
package org.springframework.security.config.http package org.springframework.security.config.http
import org.springframework.security.web.csrf.CsrfFilter
import org.springframework.security.web.header.HeaderWriterFilter import org.springframework.security.web.header.HeaderWriterFilter
import java.security.Principal import java.security.Principal
@ -107,6 +108,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
assert filters.next() instanceof SecurityContextPersistenceFilter assert filters.next() instanceof SecurityContextPersistenceFilter
assert filters.next() instanceof WebAsyncManagerIntegrationFilter assert filters.next() instanceof WebAsyncManagerIntegrationFilter
assert filters.next() instanceof HeaderWriterFilter assert filters.next() instanceof HeaderWriterFilter
assert filters.next() instanceof CsrfFilter
assert filters.next() instanceof LogoutFilter assert filters.next() instanceof LogoutFilter
Object authProcFilter = filters.next(); Object authProcFilter = filters.next();
assert authProcFilter instanceof UsernamePasswordAuthenticationFilter assert authProcFilter instanceof UsernamePasswordAuthenticationFilter
@ -187,7 +189,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
createAppContext() createAppContext()
expect: expect:
getFilters("/anything")[7] instanceof AnonymousAuthenticationFilter getFilters("/anything")[8] instanceof AnonymousAuthenticationFilter
} }
def anonymousFilterIsRemovedIfDisabledFlagSet() { def anonymousFilterIsRemovedIfDisabledFlagSet() {
@ -360,7 +362,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
AUTO_CONFIG_FILTERS + 3 == filters.size(); AUTO_CONFIG_FILTERS + 3 == filters.size();
filters[0] instanceof SecurityContextHolderAwareRequestFilter filters[0] instanceof SecurityContextHolderAwareRequestFilter
filters[1] instanceof SecurityContextPersistenceFilter filters[1] instanceof SecurityContextPersistenceFilter
filters[6] instanceof SecurityContextHolderAwareRequestFilter filters[7] instanceof SecurityContextHolderAwareRequestFilter
filters[1] instanceof SecurityContextPersistenceFilter filters[1] instanceof SecurityContextPersistenceFilter
} }
@ -383,7 +385,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
createAppContext() createAppContext()
expect: expect:
getFilters("/someurl")[4] instanceof X509AuthenticationFilter getFilters("/someurl")[5] instanceof X509AuthenticationFilter
} }
def x509SubjectPrincipalRegexCanBeSetUsingPropertyPlaceholder() { def x509SubjectPrincipalRegexCanBeSetUsingPropertyPlaceholder() {
@ -420,21 +422,9 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
def handlers = getFilter(LogoutFilter).handlers def handlers = getFilter(LogoutFilter).handlers
expect: expect:
handlers[1] instanceof CookieClearingLogoutHandler handlers[2] instanceof CookieClearingLogoutHandler
handlers[1].cookiesToClear[0] == 'JSESSIONID' handlers[2].cookiesToClear[0] == 'JSESSIONID'
handlers[1].cookiesToClear[1] == 'mycookie' handlers[2].cookiesToClear[1] == 'mycookie'
}
def invalidLogoutUrlIsDetected() {
when:
xml.http {
'logout'('logout-url': 'noLeadingSlash')
'form-login'()
}
createAppContext()
then:
BeanCreationException e = thrown();
} }
def logoutSuccessHandlerIsSetCorrectly() { def logoutSuccessHandlerIsSetCorrectly() {
@ -615,6 +605,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
xml.debug() xml.debug()
xml.http() { xml.http() {
'form-login'() 'form-login'()
csrf(disabled:true)
anonymous(enabled: 'false') anonymous(enabled: 'false')
} }
createAppContext() createAppContext()

View File

@ -74,9 +74,11 @@ class MultiHttpBlockConfigTests extends AbstractHttpConfigTests {
setup: setup:
xml.http('authentication-manager-ref' : 'authManager', 'pattern' : '/first/**') { xml.http('authentication-manager-ref' : 'authManager', 'pattern' : '/first/**') {
'form-login'('login-processing-url': '/first/login') 'form-login'('login-processing-url': '/first/login')
csrf(disabled:true)
} }
xml.http('authentication-manager-ref' : 'authManager2') { xml.http('authentication-manager-ref' : 'authManager2') {
'form-login'() 'form-login'()
csrf(disabled:true)
} }
mockBean(UserDetailsService,'uds') mockBean(UserDetailsService,'uds')
mockBean(UserDetailsService,'uds2') mockBean(UserDetailsService,'uds2')

View File

@ -91,6 +91,7 @@ class RememberMeConfigTests extends AbstractHttpConfigTests {
def rememberMeServiceWorksWithExternalServicesImpl() { def rememberMeServiceWorksWithExternalServicesImpl() {
httpAutoConfig () { httpAutoConfig () {
'remember-me'('key': "#{'our' + 'key'}", 'services-ref': 'rms') 'remember-me'('key': "#{'our' + 'key'}", 'services-ref': 'rms')
csrf(disabled:true)
} }
xml.'b:bean'(id: 'rms', 'class': TokenBasedRememberMeServices.class.name) { xml.'b:bean'(id: 'rms', 'class': TokenBasedRememberMeServices.class.name) {
'b:constructor-arg'(value: 'ourKey') 'b:constructor-arg'(value: 'ourKey')
@ -118,6 +119,7 @@ class RememberMeConfigTests extends AbstractHttpConfigTests {
def rememberMeAddsLogoutHandlerToLogoutFilter() { def rememberMeAddsLogoutHandlerToLogoutFilter() {
httpAutoConfig () { httpAutoConfig () {
'remember-me'() 'remember-me'()
csrf(disabled:true)
} }
createAppContext(AUTH_PROVIDER_XML) createAppContext(AUTH_PROVIDER_XML)

View File

@ -47,7 +47,7 @@ class SecurityContextHolderAwareRequestConfigTests extends AbstractHttpConfigTes
def withAutoConfig() { def withAutoConfig() {
httpAutoConfig () { httpAutoConfig () {
csrf(disabled:true)
} }
createAppContext(AUTH_PROVIDER_XML) createAppContext(AUTH_PROVIDER_XML)
@ -93,10 +93,12 @@ class SecurityContextHolderAwareRequestConfigTests extends AbstractHttpConfigTes
xml.http('authentication-manager-ref' : 'authManager', 'pattern' : '/first/**') { xml.http('authentication-manager-ref' : 'authManager', 'pattern' : '/first/**') {
'form-login'('login-page' : '/login') 'form-login'('login-page' : '/login')
'logout'('invalidate-session' : 'true') 'logout'('invalidate-session' : 'true')
csrf(disabled:true)
} }
xml.http('authentication-manager-ref' : 'authManager2') { xml.http('authentication-manager-ref' : 'authManager2') {
'form-login'('login-page' : '/login2') 'form-login'('login-page' : '/login2')
'logout'('invalidate-session' : 'false') 'logout'('invalidate-session' : 'false')
csrf(disabled:true)
} }
String secondAuthManager = AUTH_PROVIDER_XML.replace("alias='authManager'", "id='authManager2'") String secondAuthManager = AUTH_PROVIDER_XML.replace("alias='authManager'", "id='authManager2'")
@ -125,6 +127,7 @@ class SecurityContextHolderAwareRequestConfigTests extends AbstractHttpConfigTes
xml.http() { xml.http() {
'form-login'('login-page' : '/login') 'form-login'('login-page' : '/login')
'logout'('invalidate-session' : 'false', 'logout-success-url' : '/login?logout', 'delete-cookies' : 'JSESSIONID') 'logout'('invalidate-session' : 'false', 'logout-success-url' : '/login?logout', 'delete-cookies' : 'JSESSIONID')
csrf(disabled:true)
} }
createAppContext(AUTH_PROVIDER_XML) createAppContext(AUTH_PROVIDER_XML)

View File

@ -111,6 +111,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
'session-management'() { 'session-management'() {
'concurrency-control'('max-sessions':1,'error-if-maximum-exceeded':'true') 'concurrency-control'('max-sessions':1,'error-if-maximum-exceeded':'true')
} }
csrf(disabled:true)
} }
createAppContext() createAppContext()
SessionRegistry registry = appContext.getBean(SessionRegistry) SessionRegistry registry = appContext.getBean(SessionRegistry)
@ -155,6 +156,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
'session-management'() { 'session-management'() {
'concurrency-control'('session-registry-alias':'sr', 'expired-url': '/expired') 'concurrency-control'('session-registry-alias':'sr', 'expired-url': '/expired')
} }
csrf(disabled:true)
} }
createAppContext(); createAppContext();
List filters = getFilters("/someurl"); List filters = getFilters("/someurl");
@ -182,8 +184,9 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
} }
'logout'('invalidate-session': false, 'delete-cookies': 'testCookie') 'logout'('invalidate-session': false, 'delete-cookies': 'testCookie')
'remember-me'() 'remember-me'()
csrf(disabled:true)
} }
createAppContext(); createAppContext()
List filters = getFilters("/someurl") List filters = getFilters("/someurl")
ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) ConcurrentSessionFilter concurrentSessionFilter = filters.get(1)
@ -206,6 +209,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
'concurrency-control'() 'concurrency-control'()
} }
'remember-me'() 'remember-me'()
csrf(disabled:true)
}) })
bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint')
createAppContext() createAppContext()
@ -274,6 +278,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
setup: setup:
httpAutoConfig { httpAutoConfig {
'session-management'('session-authentication-strategy-ref':'ss') 'session-management'('session-authentication-strategy-ref':'ss')
csrf(disabled:true)
} }
mockBean(SessionAuthenticationStrategy,'ss') mockBean(SessionAuthenticationStrategy,'ss')
createAppContext() createAppContext()
@ -298,6 +303,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
'session-management'() { 'session-management'() {
'concurrency-control'('session-registry-ref':'sr') 'concurrency-control'('session-registry-ref':'sr')
} }
csrf(disabled:true)
} }
bean('sr', SessionRegistryImpl.class.name) bean('sr', SessionRegistryImpl.class.name)
createAppContext(); createAppContext();
@ -354,6 +360,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
def disablingSessionProtectionRemovesSessionManagementFilterIfNoInvalidSessionUrlSet() { def disablingSessionProtectionRemovesSessionManagementFilterIfNoInvalidSessionUrlSet() {
httpAutoConfig { httpAutoConfig {
'session-management'('session-fixation-protection': 'none') 'session-management'('session-fixation-protection': 'none')
csrf(disabled:true)
} }
createAppContext() createAppContext()
@ -364,6 +371,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() { def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() {
httpAutoConfig { httpAutoConfig {
'session-management'('session-fixation-protection': 'none', 'invalid-session-url': '/timeoutUrl') 'session-management'('session-fixation-protection': 'none', 'invalid-session-url': '/timeoutUrl')
csrf(disabled:true)
} }
createAppContext() createAppContext()
def filter = getFilters("/someurl")[10] def filter = getFilters("/someurl")[10]

View File

@ -3087,18 +3087,13 @@ This is not a limitation of Spring Security's support, but instead a general req
==== Configure CSRF Protection ==== Configure CSRF Protection
The next step is to include Spring Security's CSRF protection within your application. Some frameworks handle invalid CSRF tokens by invaliding the user's session, but this causes <<csrf-logout,its own problems>>. Instead by default Spring Security's CSRF protection will produce an HTTP 403 access denied. This can be customized by configuring the <<access-denied-handler,AccessDeniedHandler>> to process `InvalidCsrfTokenException` differently. The next step is to include Spring Security's CSRF protection within your application. Some frameworks handle invalid CSRF tokens by invaliding the user's session, but this causes <<csrf-logout,its own problems>>. Instead by default Spring Security's CSRF protection will produce an HTTP 403 access denied. This can be customized by configuring the <<access-denied-handler,AccessDeniedHandler>> to process `InvalidCsrfTokenException` differently.
For passivity reasons, if you are using the XML configuration, CSRF protection must be explicitly enabled using the <<nsa-csrf,<csrf>>> element. Refer to the <<nsa-csrf,<csrf>>> element's documentation for additional customizations. As of Spring Security 4.0, CSRF protection is enabled by default with XML configuration. If you would like to disable CSRF protection, the corresponding XML configuration can be seen below.
[NOTE]
====
https://jira.springsource.org/browse/SEC-2347[SEC-2347] is logged to ensure Spring Security 4.x's XML namespace configuration will enable CSRF protection by default.
====
[source,xml] [source,xml]
---- ----
<http> <http>
<!-- ... --> <!-- ... -->
<csrf /> <csrf disabled="true"/>
</http> </http>
---- ----
@ -6895,6 +6890,9 @@ This element will add http://en.wikipedia.org/wiki/Cross-site_request_forgery[Cr
[[nsa-csrf-attributes]] [[nsa-csrf-attributes]]
===== <csrf> Attributes ===== <csrf> Attributes
[[nsa-csrf-disabled]]
* **disabled**
Optional attribute that specifies to disable Spring Security's CSRF protection. The default is false (CSRF protection is enabled). It is highly recommended to leave CSRF protection enabled.
[[nsa-csrf-token-repository-ref]] [[nsa-csrf-token-repository-ref]]
* **token-repository-ref** * **token-repository-ref**