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 55c8f81ab8d..833efe4e22b 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 @@ -19,9 +19,11 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.function.Supplier; import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.ApplicationContext; import org.springframework.security.access.AccessDeniedException; @@ -34,13 +36,17 @@ import org.springframework.security.web.access.DelegatingAccessDeniedHandler; import org.springframework.security.web.access.ObservationMarkingAccessDeniedHandler; 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.CsrfFilter; 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.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfTokenRequestHandler; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; 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.InvalidSessionStrategy; import org.springframework.security.web.util.matcher.AndRequestMatcher; @@ -48,6 +54,7 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Adds @@ -214,6 +221,21 @@ public CsrfConfigurer sessionAuthenticationStrategy( return this; } + /** + *

+ * 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. + *

+ * @return the {@link CsrfConfigurer} for further customizations + * @since 7.0 + */ + public CsrfConfigurer spa() { + this.csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); + this.requestHandler = new SpaCsrfTokenRequestHandler(); + return this; + } + @SuppressWarnings("unchecked") @Override public void configure(H http) { @@ -375,4 +397,42 @@ protected IgnoreCsrfProtectionRegistry chainRequestMatchers(List } + 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) { + /* + * 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); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java index 216579b64df..04a288078fa 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java +++ b/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.MockMvcRequestBuilders.put; 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.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -613,6 +614,37 @@ public void getWhenHttpBasicAndCookieCsrfTokenRepositorySetAndNoExistingCookieTh 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 static class AllowHttpMethodsFirewallConfig { @@ -1006,6 +1038,18 @@ void configure(AuthenticationManagerBuilder auth) throws Exception { } + @Configuration + @EnableWebSecurity + static class CsrfSpaConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(CsrfConfigurer::spa); + return http.build(); + } + + } + @Configuration @EnableWebSecurity static class HttpBasicCsrfTokenRequestHandlerConfig { diff --git a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc index ab4af97ddeb..f02350dc6d1 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc @@ -787,48 +787,10 @@ public class SecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // ... - .csrf((csrf) -> csrf - .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <1> - .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // <2> - ); + .csrf((csrf) -> csrf.spa()); 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) { - /* - * 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:: @@ -846,51 +808,12 @@ class SecurityConfig { http { // ... csrf { - csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() // <1> - csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2> + spa() } } 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) { - /* - * 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:: @@ -899,22 +822,13 @@ XML:: ---- - - request-handler-ref="requestHandler"/> <2> + + + - - ---- ====== -<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]] ==== Multi-Page Applications