From ee9f5a2d5ecd05c841b88c778a723f64435c8413 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Wed, 29 May 2024 11:52:09 -0500 Subject: [PATCH] Improve CSRF example for single-page apps Closes gh-15105 --- .../ROOT/pages/servlet/exploits/csrf.adoc | 81 +++++++------------ 1 file changed, 30 insertions(+), 51 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc index 3dd8e7a1806..7dd0a4cea0e 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc @@ -788,14 +788,14 @@ public class SecurityConfig { .csrf((csrf) -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <1> .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // <2> - ) - .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); // <3> + ); return http.build(); } } -final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler { - private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler(); +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) { @@ -803,40 +803,28 @@ final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of * the CsrfToken when it is rendered in the response body. */ - this.delegate.handle(request, response, csrfToken); + 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. - */ - if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) { - return super.resolveCsrfTokenValue(request, 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 this.delegate.resolveCsrfTokenValue(request, csrfToken); - } -} - -final class CsrfCookieFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf"); - // Render the token value to a cookie by causing the deferred token to be loaded - csrfToken.getToken(); - - filterChain.doFilter(request, response); + return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken); } } ---- @@ -856,35 +844,40 @@ class SecurityConfig { http { // ... csrf { - csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() // <1> - csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2> + csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() // <1> + csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2> } } - http.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java) // <3> return http.build() } } -class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() { - private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler() +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. */ - delegate.handle(request, response, csrfToken) + 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(request.getHeader(csrfToken.headerName))) { - super.resolveCsrfTokenValue(request, csrfToken) + return if (StringUtils.hasText(headerValue)) { + plain } else { /* * In all other cases (e.g. if the request contains a request parameter), use @@ -892,19 +885,8 @@ class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() { * when a server-side rendered form includes the _csrf request parameter as a * hidden input. */ - delegate.resolveCsrfTokenValue(request, csrfToken) - } - } -} - -class CsrfCookieFilter : OncePerRequestFilter() { - - @Throws(ServletException::class, IOException::class) - override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { - val csrfToken = request.getAttribute("_csrf") as CsrfToken - // Render the token value to a cookie by causing the deferred token to be loaded - csrfToken.token - filterChain.doFilter(request, response) + xor + }.resolveCsrfTokenValue(request, csrfToken) } } ---- @@ -916,23 +898,20 @@ XML:: - request-handler-ref="requestHandler"/> <2> - <3> + token-repository-ref="tokenRepository" <1> + 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`). -<3> Configure a custom `Filter` to load the `CsrfToken` on every request, which will return a new cookie if needed. + 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