Browse Source

Create CsrfCustomizer for SPA configuration

Closes gh-14149

Signed-off-by: Felix Hagemans <felixhagemans@gmail.com>
pull/16964/head
Felix Hagemans 9 months ago committed by Josh Cummings
parent
commit
1a4de49977
  1. 60
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java
  2. 44
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java
  3. 96
      docs/modules/ROOT/pages/servlet/exploits/csrf.adoc

60
config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java

@ -19,9 +19,11 @@ package org.springframework.security.config.annotation.web.configurers;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
@ -34,13 +36,17 @@ import org.springframework.security.web.access.CompositeAccessDeniedHandler;
import org.springframework.security.web.access.DelegatingAccessDeniedHandler; import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
import org.springframework.security.web.access.ObservationMarkingAccessDeniedHandler; import org.springframework.security.web.access.ObservationMarkingAccessDeniedHandler;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy; import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfLogoutHandler; import org.springframework.security.web.csrf.CsrfLogoutHandler;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler; import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.csrf.MissingCsrfTokenException; import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler; import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
import org.springframework.security.web.session.InvalidSessionStrategy; import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AndRequestMatcher;
@ -48,6 +54,7 @@ import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/** /**
* Adds * Adds
@ -214,6 +221,21 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
return this; return this;
} }
/**
* <p>
* Sensible CSRF defaults when used in combination with a single page application.
* Creates a cookie-based token repository and a custom request handler to resolve the
* actual token value instead of the encoded token.
* </p>
* @return the {@link CsrfConfigurer} for further customizations
* @since 7.0
*/
public CsrfConfigurer<H> spa() {
this.csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
this.requestHandler = new SpaCsrfTokenRequestHandler();
return this;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public void configure(H http) { public void configure(H http) {
@ -375,4 +397,42 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
} }
private static class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestAttributeHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestAttributeHandler xor = new XorCsrfTokenRequestAttributeHandler();
SpaCsrfTokenRequestHandler() {
this.xor.setCsrfRequestAttributeName(null);
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection
* of the CsrfToken when it is rendered in the response body.
*/
this.xor.handle(request, response, csrfToken);
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
/*
* If the request contains a request header, use
* CsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a single-page application includes the header value automatically,
* which was obtained via a cookie containing the raw CsrfToken.
*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
}
}
} }

44
config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java

@ -93,6 +93,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -613,6 +614,37 @@ public class CsrfConfigurerTests {
assertThat(cookies).isEmpty(); assertThat(cookies).isEmpty();
} }
@Test
public void spaConfigForbidden() throws Exception {
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
.autowire();
this.mvc.perform(post("/")).andExpect(status().isForbidden());
}
@Test
public void spaConfigOk() throws Exception {
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
.autowire();
this.mvc.perform(post("/").with(csrf())).andExpect(status().isOk());
}
@Test
public void spaConfigDoubleSubmit() throws Exception {
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
.autowire();
var token = this.mvc.perform(post("/"))
.andExpect(status().isForbidden())
.andExpect(cookie().exists("XSRF-TOKEN"))
.andReturn()
.getResponse()
.getCookie("XSRF-TOKEN");
this.mvc
.perform(post("/").header("X-XSRF-TOKEN", token.getValue())
.cookie(new Cookie("XSRF-TOKEN", token.getValue())))
.andExpect(status().isOk());
}
@Configuration @Configuration
static class AllowHttpMethodsFirewallConfig { static class AllowHttpMethodsFirewallConfig {
@ -1006,6 +1038,18 @@ public class CsrfConfigurerTests {
} }
@Configuration
@EnableWebSecurity
static class CsrfSpaConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(CsrfConfigurer::spa);
return http.build();
}
}
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
static class HttpBasicCsrfTokenRequestHandlerConfig { static class HttpBasicCsrfTokenRequestHandlerConfig {

96
docs/modules/ROOT/pages/servlet/exploits/csrf.adoc

@ -787,48 +787,10 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
// ... // ...
.csrf((csrf) -> csrf .csrf((csrf) -> csrf.spa());
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <1>
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // <2>
);
return http.build(); return http.build();
} }
} }
final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
this.xor.handle(request, response, csrfToken);
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get();
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
}
}
---- ----
Kotlin:: Kotlin::
@ -846,51 +808,12 @@ class SecurityConfig {
http { http {
// ... // ...
csrf { csrf {
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() // <1> spa()
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2>
} }
} }
return http.build() return http.build()
} }
} }
class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
private val xor: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
xor.handle(request, response, csrfToken)
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get()
}
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
val headerValue = request.getHeader(csrfToken.headerName)
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*/
return if (StringUtils.hasText(headerValue)) {
plain
} else {
/*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
xor
}.resolveCsrfTokenValue(request, csrfToken)
}
}
---- ----
XML:: XML::
@ -899,22 +822,13 @@ XML::
---- ----
<http> <http>
<!-- ... --> <!-- ... -->
<csrf <csrf>
token-repository-ref="tokenRepository" <1> <spa />
request-handler-ref="requestHandler"/> <2> </csrf>
</http> </http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
class="example.SpaCsrfTokenRequestHandler"/>
---- ----
====== ======
<1> Configure `CookieCsrfTokenRepository` with `HttpOnly` set to `false` so the cookie can be read by the JavaScript application.
<2> Configure a custom `CsrfTokenRequestHandler` that resolves the CSRF token based on whether it is an HTTP request header (`X-XSRF-TOKEN`) or request parameter (`_csrf`).
This implementation also causes the deferred `CsrfToken` to be loaded on every request, which will return a new cookie if needed.
[[csrf-integration-javascript-mpa]] [[csrf-integration-javascript-mpa]]
==== Multi-Page Applications ==== Multi-Page Applications

Loading…
Cancel
Save