Quellcode durchsuchen

Create CsrfCustomizer for SPA configuration

Closes gh-14149

Signed-off-by: Felix Hagemans <felixhagemans@gmail.com>
Felix Hagemans vor 4 Monaten
Ursprung
Commit
1a4de49977

+ 60 - 0
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.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.CompositeAccessDeniedHandler;
 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.NegatedRequestMatcher;
 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 final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
 		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")
 	@Override
 	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 - 0
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.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 class CsrfConfigurerTests {
 		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 @@ public class CsrfConfigurerTests {
 
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	static class CsrfSpaConfig {
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+			http.csrf(CsrfConfigurer::spa);
+			return http.build();
+		}
+
+	}
+
 	@Configuration
 	@EnableWebSecurity
 	static class HttpBasicCsrfTokenRequestHandlerConfig {

+ 5 - 91
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> 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<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::
@@ -899,22 +822,13 @@ XML::
 ----
 <http>
 	<!-- ... -->
-	<csrf
-		token-repository-ref="tokenRepository"                                        <1>
-		request-handler-ref="requestHandler"/>                                        <2>
+	<csrf>
+        <spa />
+    </csrf>
 </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]]
 ==== Multi-Page Applications