From 45b81b194b9bd9f5ff292f3145f01a009aff8c75 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 15 Feb 2023 14:53:28 -0600 Subject: [PATCH 1/5] Expand migration docs regarding CSRF Closes gh-12462 --- .../pages/migration/servlet/exploits.adoc | 190 +++++++++++++++++- 1 file changed, 186 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/migration/servlet/exploits.adoc b/docs/modules/ROOT/pages/migration/servlet/exploits.adoc index 379c1f83ac..15e6716f6b 100644 --- a/docs/modules/ROOT/pages/migration/servlet/exploits.adoc +++ b/docs/modules/ROOT/pages/migration/servlet/exploits.adoc @@ -7,8 +7,20 @@ The following steps relate to changes around how to configure CSRF. In Spring Security 5, the default behavior is that the `CsrfToken` will be loaded on every request. This means that in a typical setup, the `HttpSession` must be read for every request even if it is unnecessary. +[NOTE] +==== +Some examples of where it should be unnecessary to read the session include endpoints marked `permitAll()` such as static assets, static HTML pages, single-page applications hosted under the same domain/server, etc. +==== + In Spring Security 6, the default is that the lookup of the `CsrfToken` will be deferred until it is needed. +[NOTE] +==== +The `CsrfToken` is needed whenever a request is made with an HTTP verb that would change the state of the application. +This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent]. +Additionally, it is needed by any request that renders the token to the response, such as a web page with a `
` tag that includes a hidden `` for the CSRF token. +==== + To opt into the new Spring Security 6 default, the following configuration can be used. [[servlet-opt-in-defer-loading-csrf-token]] @@ -18,7 +30,7 @@ To opt into the new Spring Security 6 default, the following configuration can b [source,java,role="primary"] ---- @Bean -DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { +public SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); // set the name of the attribute the CsrfToken will be populated on requestHandler.setCsrfRequestAttributeName("_csrf"); @@ -61,7 +73,156 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain { ---- ==== -If this breaks your application, then you can explicitly opt into the 5.8 defaults using the following configuration: +[NOTE] +==== +When the `CsrfToken` is deferred (the default in Spring Security 6), some applications may break due to the fact that they were designed with non-deferred CSRF tokens. +See <> below for more information. +==== + +[[servlet-defer-loading-csrf-token-opt-out]] +=== Opt-out Steps + +If configuring the `CsrfToken` to be deferred gives you trouble, take a look at these scenarios for optimal opt out behavior: + +==== I am using a Single-Page Application with `CookieCsrfTokenRepository` + +If you are using a single-page app (SPA) to connect to a backend protected by Spring Security along with `CookieCsrfTokenRepository.withHttpOnlyFalse()`, you may find that the CSRF token is no longer returned to your application as a cookie on the first request to the server. + +In this case, you have several options for restoring the behavior your client-side application expects. +One option is to add a `Filter` that eagerly renders the `CsrfToken` to the response regardless of which request is made first, like so: + +.Add a `Filter` to return a cookie on the response +==== +.Java +[source,java,role="primary"] +---- +@Bean +public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); + CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); + // set the name of the attribute the CsrfToken will be populated on + requestHandler.setCsrfRequestAttributeName("_csrf"); + http + // ... + .csrf((csrf) -> csrf + .csrfTokenRepository(tokenRepository) + .csrfTokenRequestHandler(requestHandler) + ) + .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); + + return http.build(); +} + +private static final class CsrfCookieFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + // Render the token value to a cookie by causing the deferred token to be loaded + csrfToken.getToken(); + + filterChain.doFilter(request, response); + } + +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + val tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() + val requestHandler = CsrfTokenRequestAttributeHandler() + // set the name of the attribute the CsrfToken will be populated on + requestHandler.setCsrfRequestAttributeName("_csrf") + http { + csrf { + csrfTokenRepository = tokenRepository + csrfTokenRequestHandler = requestHandler + } + addFilterAfter(CsrfCookieFilter()) + } + return http.build() +} + +class CsrfCookieFilter : OncePerRequestFilter() { + + override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { + val csrfToken = request.getAttribute(CsrfToken::class.java.name) as CsrfToken + // Render the token value to a cookie by causing the deferred token to be loaded + csrfToken.token + + filterChain.doFilter(request, response) + } + +} +---- +==== + +The option above does not require changes to the single-page application, but does cause the `CsrfToken` to be loaded on every request. +If you do not wish to add a `Filter` to eagerly load tokens on every request, additional options are listed below. + +==== I am using a Single-Page Application with `HttpSessionCsrfTokenRepository` + +If you are using sessions, your application will benefit from deferred tokens. +Instead of opting out, another option is to add a new `@RestController` with a `/csrf` endpoint, like so: + +.Add a `/csrf` endpoint +==== +.Java +[source,java,role="primary"] +---- +@RestController +public class CsrfController { + + @GetMapping("/csrf") + public CsrfToken csrf(CsrfToken csrfToken) { + return csrfToken; + } + +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@RestController +class CsrfController { + + @GetMapping("/csrf") + fun csrf(csrfToken: CsrfToken): CsrfToken { + return csrfToken + } + +} +---- +==== + +[NOTE] +==== +You may consider adding `.requestMatchers("/csrf").permitAll()` if the endpoint above is required prior to authenticating with the server. +==== + +The `/csrf` endpoint would need to be consumed by the client-side application in order to bootstrap the application for subsequent requests. + +[NOTE] +==== +Instructions for calling the `/csrf` endpoint on application launch are specific to your client-side framework and therefore outside the scope of this document. +==== + +[NOTE] +==== +While this requires changes to your single-page application, the benefit is that the CSRF token is only loaded once and the token can continue to be deferred. +This approach works particularly well with applications that use `HttpSessionCsrfTokenRepository` and do benefit from deferred tokens by allowing the `HttpSession` not to be read on every request. +==== + +If you simply wish to opt out of deferred tokens altogether, that option is listed next. + +==== I need to opt out of deferred tokens for another reason + +If deferred tokens break your application for another reason, then you can explicitly opt into the 5.8 defaults using the following configuration: .Explicit Configure `CsrfToken` with 5.8 Defaults ==== @@ -69,7 +230,7 @@ If this breaks your application, then you can explicitly opt into the 5.8 defaul [source,java,role="primary"] ---- @Bean -DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { +public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); // set the name of the attribute the CsrfToken will be populated on requestHandler.setCsrfRequestAttributeName(null); @@ -86,7 +247,7 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { [source,kotlin,role="secondary"] ---- @Bean -open fun springSecurity(http: HttpSecurity): SecurityFilterChain { +open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { val requestHandler = CsrfTokenRequestAttributeHandler() // set the name of the attribute the CsrfToken will be populated on requestHandler.setCsrfRequestAttributeName(null) @@ -115,6 +276,12 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain { ---- ==== +[NOTE] +==== +By setting the `csrfRequestAttributeName` to `null`, the `CsrfToken` must first be loaded to determine what attribute name to use. +This causes the `CsrfToken` to be loaded on every request. +==== + == Protect against CSRF BREACH If the steps for <> work for you, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` using the following configuration: @@ -240,6 +407,21 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain { ---- ==== +This is the RECOMMENDED way to configure Spring Security to work with a client-side application that uses cookie values, because it continues to allow the response to return a randomized value for the CSRF token in case the application returns HTML or other responses that could be vulnerable to BREACH without your knowledge. + +[NOTE] +==== +BREACH protection works to protect the token when it is included in a response body that can be GZIP compressed, which generally does not include headers and cookies. +==== + +[TIP] +==== +Any token value returned by the server can be used successfully by the client-side application because the underlying (raw) CSRF token does not change. +It is not required for an AngularJS (or similar) application to refresh the CSRF token before/after every request. +==== + +If you simply wish to opt out of CSRF BREACH protection altogether, that option is listed next. + ==== I need to opt out of CSRF BREACH protection for another reason If CSRF BREACH protection does not work for you for another reason, you can opt out using the configuration from the <> section. From 821db0a1ea6d91a612eaa1b8a468a1ccaafc650c Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 15 Feb 2023 17:00:49 -0600 Subject: [PATCH 2/5] Polish migration doc Issue gh-12675 --- docs/modules/ROOT/pages/migration/index.adoc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/migration/index.adoc b/docs/modules/ROOT/pages/migration/index.adoc index 58aef9b95d..0fd53e5096 100644 --- a/docs/modules/ROOT/pages/migration/index.adoc +++ b/docs/modules/ROOT/pages/migration/index.adoc @@ -4,12 +4,11 @@ The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0. Use 5.8 and the steps below to minimize changes when ifdef::spring-security-version[] -xref:6.0.0@migration/index.adoc[updating to 6.0] +xref:6.0.0@migration/index.adoc[updating to 6.0]. endif::[] ifndef::spring-security-version[] -updating to 6.0 +updating to 6.0. endif::[] -. == Update to Spring Security 5.8 From ca1961d35ecc07c51f05fbffb9d5e0ff25164be2 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 15 Feb 2023 17:01:28 -0600 Subject: [PATCH 3/5] Link to the latest 6.0.x release Issue gh-12675 --- docs/modules/ROOT/pages/migration/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/migration/index.adoc b/docs/modules/ROOT/pages/migration/index.adoc index 0fd53e5096..061d60823e 100644 --- a/docs/modules/ROOT/pages/migration/index.adoc +++ b/docs/modules/ROOT/pages/migration/index.adoc @@ -4,7 +4,7 @@ The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0. Use 5.8 and the steps below to minimize changes when ifdef::spring-security-version[] -xref:6.0.0@migration/index.adoc[updating to 6.0]. +xref:6.0.2@migration/index.adoc[updating to 6.0]. endif::[] ifndef::spring-security-version[] updating to 6.0. From bf2951b5af180b1ee003820ad8bd318514d1a949 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 15 Feb 2023 15:25:43 -0600 Subject: [PATCH 4/5] Add sections for migrating exploit protection in 6.0 Issue gh-12462 --- docs/modules/ROOT/pages/migration/index.adoc | 2 +- .../pages/migration/servlet/exploits.adoc | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/migration/index.adoc b/docs/modules/ROOT/pages/migration/index.adoc index 56b66e42e5..77a00c953e 100644 --- a/docs/modules/ROOT/pages/migration/index.adoc +++ b/docs/modules/ROOT/pages/migration/index.adoc @@ -4,7 +4,7 @@ The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0. Use 5.8 and ifdef::spring-security-version[] -xref:5.8.0@migration/index.adoc[its preparation steps] +xref:5.8.2@migration/index.adoc[its preparation steps] endif::[] ifndef::spring-security-version[] its preparation steps diff --git a/docs/modules/ROOT/pages/migration/servlet/exploits.adoc b/docs/modules/ROOT/pages/migration/servlet/exploits.adoc index 35b49b325e..e27fd58719 100644 --- a/docs/modules/ROOT/pages/migration/servlet/exploits.adoc +++ b/docs/modules/ROOT/pages/migration/servlet/exploits.adoc @@ -1,7 +1,39 @@ = Exploit Protection Migrations +The 5.8 migration guide contains several steps for +ifdef::spring-security-version[] +xref:5.8.2@migration/servlet/exploits.adoc[exploit protection migrations] when updating to 6.0. +endif::[] +ifndef::spring-security-version[] +exploit protection migrations when updating to 6.0. +endif::[] +You are encouraged to follow those steps first. + The following steps relate to how to finish migrating exploit protection support. +== Defer Loading CsrfToken + +In Spring Security 5.8, the default `CsrfTokenRequestHandler` for making the `CsrfToken` available to the application is `CsrfTokenRequestAttributeHandler`. +The default for the field `csrfRequestAttributeName` is `null`, which causes the CSRF token to be loaded on every request. + +In Spring Security 6, `csrfRequestAttributeName` defaults to `_csrf`. +If you configured the following only for the purpose of updating to 6.0, you can now remove it: + + requestHandler.setCsrfRequestAttributeName("_csrf"); + +== Protect against CSRF BREACH + +In Spring Security 5.8, the default `CsrfTokenRequestHandler` for making the `CsrfToken` available to the application is `CsrfTokenRequestAttributeHandler`. +`XorCsrfTokenRequestAttributeHandler` was added to allow opting into CSRF BREACH support. + +In Spring Security 6, `XorCsrfTokenRequestAttributeHandler` is the default `CsrfTokenRequestHandler` for making the `CsrfToken` available. +If you configured the `XorCsrfTokenRequestAttributeHandler` only for the purpose of updating to 6.0, you can remove it completely. + +[NOTE] +==== +If you have set the `csrfRequestAttributeName` to `null` in order to opt out of deferred tokens, or if you have configured a `CsrfTokenRequestHandler` for any other reason, you can leave the configuration in place. +==== + == CSRF BREACH with WebSocket support In Spring Security 5.8, the default `ChannelInterceptor` for making the `CsrfToken` available with xref:servlet/integrations/websocket.adoc[WebSocket Security] is `CsrfChannelInterceptor`. From 28766053242bc0ab6cc57e0695daf8e61106ca80 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 15 Feb 2023 17:18:00 -0600 Subject: [PATCH 5/5] Polish migration doc Issue gh-12675 --- docs/modules/ROOT/pages/migration/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/migration/index.adoc b/docs/modules/ROOT/pages/migration/index.adoc index 77a00c953e..675bfafe94 100644 --- a/docs/modules/ROOT/pages/migration/index.adoc +++ b/docs/modules/ROOT/pages/migration/index.adoc @@ -9,7 +9,7 @@ endif::[] ifndef::spring-security-version[] its preparation steps endif::[] -to simplify updating to 6.0 +to simplify updating to 6.0. After updating to 5.8, follow this guide to perform any remaining migration or cleanup steps.