Skip to content

Commit ee9f5a2

Browse files
committed
Improve CSRF example for single-page apps
Closes gh-15105
1 parent 17064fc commit ee9f5a2

File tree

1 file changed

+30
-51
lines changed
  • docs/modules/ROOT/pages/servlet/exploits

1 file changed

+30
-51
lines changed

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

+30-51
Original file line numberDiff line numberDiff line change
@@ -788,55 +788,43 @@ public class SecurityConfig {
788788
.csrf((csrf) -> csrf
789789
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <1>
790790
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // <2>
791-
)
792-
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); // <3>
791+
);
793792
return http.build();
794793
}
795794
}
796795
797-
final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
798-
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
796+
final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
797+
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
798+
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
799799
800800
@Override
801801
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
802802
/*
803803
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
804804
* the CsrfToken when it is rendered in the response body.
805805
*/
806-
this.delegate.handle(request, response, csrfToken);
806+
this.xor.handle(request, response, csrfToken);
807+
/*
808+
* Render the token value to a cookie by causing the deferred token to be loaded.
809+
*/
810+
csrfToken.get();
807811
}
808812
809813
@Override
810814
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
815+
String headerValue = request.getHeader(csrfToken.getHeaderName());
811816
/*
812817
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
813818
* to resolve the CsrfToken. This applies when a single-page application includes
814819
* the header value automatically, which was obtained via a cookie containing the
815820
* raw CsrfToken.
816-
*/
817-
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
818-
return super.resolveCsrfTokenValue(request, csrfToken);
819-
}
820-
/*
821+
*
821822
* In all other cases (e.g. if the request contains a request parameter), use
822823
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
823824
* when a server-side rendered form includes the _csrf request parameter as a
824825
* hidden input.
825826
*/
826-
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
827-
}
828-
}
829-
830-
final class CsrfCookieFilter extends OncePerRequestFilter {
831-
832-
@Override
833-
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
834-
throws ServletException, IOException {
835-
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
836-
// Render the token value to a cookie by causing the deferred token to be loaded
837-
csrfToken.getToken();
838-
839-
filterChain.doFilter(request, response);
827+
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
840828
}
841829
}
842830
----
@@ -856,55 +844,49 @@ class SecurityConfig {
856844
http {
857845
// ...
858846
csrf {
859-
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() // <1>
860-
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2>
847+
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() // <1>
848+
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2>
861849
}
862850
}
863-
http.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java) // <3>
864851
return http.build()
865852
}
866853
}
867854
868-
class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
869-
private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
855+
class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
856+
private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
857+
private val xor: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
870858
871859
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
872860
/*
873861
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
874862
* the CsrfToken when it is rendered in the response body.
875863
*/
876-
delegate.handle(request, response, csrfToken)
864+
xor.handle(request, response, csrfToken)
865+
/*
866+
* Render the token value to a cookie by causing the deferred token to be loaded.
867+
*/
868+
csrfToken.get()
877869
}
878870
879871
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
872+
val headerValue = request.getHeader(csrfToken.headerName)
880873
/*
881874
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
882875
* to resolve the CsrfToken. This applies when a single-page application includes
883876
* the header value automatically, which was obtained via a cookie containing the
884877
* raw CsrfToken.
885878
*/
886-
return if (StringUtils.hasText(request.getHeader(csrfToken.headerName))) {
887-
super.resolveCsrfTokenValue(request, csrfToken)
879+
return if (StringUtils.hasText(headerValue)) {
880+
plain
888881
} else {
889882
/*
890883
* In all other cases (e.g. if the request contains a request parameter), use
891884
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
892885
* when a server-side rendered form includes the _csrf request parameter as a
893886
* hidden input.
894887
*/
895-
delegate.resolveCsrfTokenValue(request, csrfToken)
896-
}
897-
}
898-
}
899-
900-
class CsrfCookieFilter : OncePerRequestFilter() {
901-
902-
@Throws(ServletException::class, IOException::class)
903-
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
904-
val csrfToken = request.getAttribute("_csrf") as CsrfToken
905-
// Render the token value to a cookie by causing the deferred token to be loaded
906-
csrfToken.token
907-
filterChain.doFilter(request, response)
888+
xor
889+
}.resolveCsrfTokenValue(request, csrfToken)
908890
}
909891
}
910892
----
@@ -916,23 +898,20 @@ XML::
916898
<http>
917899
<!-- ... -->
918900
<csrf
919-
token-repository-ref="tokenRepository" <1>
920-
request-handler-ref="requestHandler"/> <2>
921-
<custom-filter ref="csrfCookieFilter" after="BASIC_AUTH_FILTER"/> <3>
901+
token-repository-ref="tokenRepository" <1>
902+
request-handler-ref="requestHandler"/> <2>
922903
</http>
923904
<b:bean id="tokenRepository"
924905
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
925906
p:cookieHttpOnly="false"/>
926907
<b:bean id="requestHandler"
927908
class="example.SpaCsrfTokenRequestHandler"/>
928-
<b:bean id="csrfCookieFilter"
929-
class="example.CsrfCookieFilter"/>
930909
----
931910
======
932911

933912
<1> Configure `CookieCsrfTokenRepository` with `HttpOnly` set to `false` so the cookie can be read by the JavaScript application.
934913
<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`).
935-
<3> Configure a custom `Filter` to load the `CsrfToken` on every request, which will return a new cookie if needed.
914+
This implementation also causes the deferred `CsrfToken` to be loaded on every request, which will return a new cookie if needed.
936915

937916
[[csrf-integration-javascript-mpa]]
938917
==== Multi-Page Applications

0 commit comments

Comments
 (0)