Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1740373

Browse files
committedApr 24, 2025·
Processed review comments and updated documentation
Signed-off-by: Felix Hagemans <[email protected]>
1 parent 8d09a9a commit 1740373

File tree

4 files changed

+141
-226
lines changed

4 files changed

+141
-226
lines changed
 

‎config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import java.util.ArrayList;
2020
import java.util.LinkedHashMap;
2121
import java.util.List;
22+
import java.util.function.Supplier;
2223

2324
import io.micrometer.observation.ObservationRegistry;
2425
import jakarta.servlet.http.HttpServletRequest;
26+
import jakarta.servlet.http.HttpServletResponse;
2527

2628
import org.springframework.context.ApplicationContext;
2729
import org.springframework.security.access.AccessDeniedException;
@@ -34,20 +36,25 @@
3436
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
3537
import org.springframework.security.web.access.ObservationMarkingAccessDeniedHandler;
3638
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
39+
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
3740
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
3841
import org.springframework.security.web.csrf.CsrfFilter;
3942
import org.springframework.security.web.csrf.CsrfLogoutHandler;
43+
import org.springframework.security.web.csrf.CsrfToken;
4044
import org.springframework.security.web.csrf.CsrfTokenRepository;
45+
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
4146
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
4247
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
4348
import org.springframework.security.web.csrf.MissingCsrfTokenException;
49+
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
4450
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
4551
import org.springframework.security.web.session.InvalidSessionStrategy;
4652
import org.springframework.security.web.util.matcher.AndRequestMatcher;
4753
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
4854
import org.springframework.security.web.util.matcher.OrRequestMatcher;
4955
import org.springframework.security.web.util.matcher.RequestMatcher;
5056
import org.springframework.util.Assert;
57+
import org.springframework.util.StringUtils;
5158

5259
/**
5360
* Adds
@@ -214,6 +221,21 @@ public CsrfConfigurer<H> sessionAuthenticationStrategy(
214221
return this;
215222
}
216223

224+
/**
225+
* <p>
226+
* Sensible CSRF defaults when used in combination with a single page application.
227+
* Creates a cookie-based token repository and a custom request handler to resolve the
228+
* actual token value instead of the encoded token.
229+
* </p>
230+
* @return the {@link CsrfConfigurer} for further customizations
231+
* @since 7.0
232+
*/
233+
public CsrfConfigurer<H> spa() {
234+
this.csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
235+
this.requestHandler = new SpaCsrfTokenRequestHandler();
236+
return this;
237+
}
238+
217239
@SuppressWarnings("unchecked")
218240
@Override
219241
public void configure(H http) {
@@ -375,4 +397,42 @@ protected IgnoreCsrfProtectionRegistry chainRequestMatchers(List<RequestMatcher>
375397

376398
}
377399

400+
private static class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
401+
402+
private final CsrfTokenRequestAttributeHandler plain = new CsrfTokenRequestAttributeHandler();
403+
404+
private final CsrfTokenRequestAttributeHandler xor = new XorCsrfTokenRequestAttributeHandler();
405+
406+
SpaCsrfTokenRequestHandler() {
407+
this.xor.setCsrfRequestAttributeName(null);
408+
}
409+
410+
@Override
411+
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
412+
/*
413+
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection
414+
* of the CsrfToken when it is rendered in the response body.
415+
*/
416+
this.xor.handle(request, response, csrfToken);
417+
}
418+
419+
@Override
420+
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
421+
String headerValue = request.getHeader(csrfToken.getHeaderName());
422+
/*
423+
* If the request contains a request header, use
424+
* CsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
425+
* when a single-page application includes the header value automatically,
426+
* which was obtained via a cookie containing the raw CsrfToken.
427+
*
428+
* In all other cases (e.g. if the request contains a request parameter), use
429+
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
430+
* when a server-side rendered form includes the _csrf request parameter as a
431+
* hidden input.
432+
*/
433+
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
434+
}
435+
436+
}
437+
378438
}

‎config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfCustomizer.java

Lines changed: 0 additions & 57 deletions
This file was deleted.

‎config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java

Lines changed: 76 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -114,72 +114,72 @@ public class CsrfConfigurerTests {
114114
@Test
115115
public void postWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
116116
this.spring
117-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
118-
.autowire();
117+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
118+
.autowire();
119119
this.mvc.perform(post("/")).andExpect(status().isForbidden());
120120
}
121121

122122
@Test
123123
public void putWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
124124
this.spring
125-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
126-
.autowire();
125+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
126+
.autowire();
127127
this.mvc.perform(put("/")).andExpect(status().isForbidden());
128128
}
129129

130130
@Test
131131
public void patchWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
132132
this.spring
133-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
134-
.autowire();
133+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
134+
.autowire();
135135
this.mvc.perform(patch("/")).andExpect(status().isForbidden());
136136
}
137137

138138
@Test
139139
public void deleteWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
140140
this.spring
141-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
142-
.autowire();
141+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
142+
.autowire();
143143
this.mvc.perform(delete("/")).andExpect(status().isForbidden());
144144
}
145145

146146
@Test
147147
public void invalidWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
148148
this.spring
149-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
150-
.autowire();
149+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
150+
.autowire();
151151
this.mvc.perform(request("INVALID", URI.create("/"))).andExpect(status().isForbidden());
152152
}
153153

154154
@Test
155155
public void getWhenWebSecurityEnabledThenRespondsWithOk() throws Exception {
156156
this.spring
157-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
158-
.autowire();
157+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
158+
.autowire();
159159
this.mvc.perform(get("/")).andExpect(status().isOk());
160160
}
161161

162162
@Test
163163
public void headWhenWebSecurityEnabledThenRespondsWithOk() throws Exception {
164164
this.spring
165-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
166-
.autowire();
165+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
166+
.autowire();
167167
this.mvc.perform(head("/")).andExpect(status().isOk());
168168
}
169169

170170
@Test
171171
public void traceWhenWebSecurityEnabledThenRespondsWithOk() throws Exception {
172172
this.spring
173-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
174-
.autowire();
173+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
174+
.autowire();
175175
this.mvc.perform(request(HttpMethod.TRACE, "/")).andExpect(status().isOk());
176176
}
177177

178178
@Test
179179
public void optionsWhenWebSecurityEnabledThenRespondsWithOk() throws Exception {
180180
this.spring
181-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
182-
.autowire();
181+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
182+
.autowire();
183183
this.mvc.perform(options("/")).andExpect(status().isOk());
184184
}
185185

@@ -209,11 +209,11 @@ public void loginWhenCsrfDisabledThenRedirectsToPreviousPostRequest() throws Exc
209209
RequestCache requestCache = new HttpSessionRequestCache();
210210
String redirectUrl = requestCache.getRequest(mvcResult.getRequest(), mvcResult.getResponse()).getRedirectUrl();
211211
this.mvc
212-
.perform(post("/login").param("username", "user")
213-
.param("password", "password")
214-
.session((MockHttpSession) mvcResult.getRequest().getSession()))
215-
.andExpect(status().isFound())
216-
.andExpect(redirectedUrl(redirectUrl));
212+
.perform(post("/login").param("username", "user")
213+
.param("password", "password")
214+
.session((MockHttpSession) mvcResult.getRequest().getSession()))
215+
.andExpect(status().isFound())
216+
.andExpect(redirectedUrl(redirectUrl));
217217
}
218218

219219
@Test
@@ -222,18 +222,18 @@ public void loginWhenCsrfEnabledThenDoesNotRedirectToPreviousPostRequest() throw
222222
DefaultCsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
223223
given(CsrfDisablesPostRequestFromRequestCacheConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
224224
any(HttpServletResponse.class)))
225-
.willReturn(new TestDeferredCsrfToken(csrfToken));
225+
.willReturn(new TestDeferredCsrfToken(csrfToken));
226226
this.spring.register(CsrfDisablesPostRequestFromRequestCacheConfig.class).autowire();
227227
MvcResult mvcResult = this.mvc.perform(post("/some-url")).andReturn();
228228
this.mvc
229-
.perform(post("/login").param("username", "user")
230-
.param("password", "password")
231-
.with(csrf())
232-
.session((MockHttpSession) mvcResult.getRequest().getSession()))
233-
.andExpect(status().isFound())
234-
.andExpect(redirectedUrl("/"));
229+
.perform(post("/login").param("username", "user")
230+
.param("password", "password")
231+
.with(csrf())
232+
.session((MockHttpSession) mvcResult.getRequest().getSession()))
233+
.andExpect(status().isFound())
234+
.andExpect(redirectedUrl("/"));
235235
verify(CsrfDisablesPostRequestFromRequestCacheConfig.REPO, atLeastOnce())
236-
.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
236+
.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
237237
}
238238

239239
@Test
@@ -242,32 +242,32 @@ public void loginWhenCsrfEnabledThenRedirectsToPreviousGetRequest() throws Excep
242242
DefaultCsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
243243
given(CsrfDisablesPostRequestFromRequestCacheConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
244244
any(HttpServletResponse.class)))
245-
.willReturn(new TestDeferredCsrfToken(csrfToken));
245+
.willReturn(new TestDeferredCsrfToken(csrfToken));
246246
this.spring.register(CsrfDisablesPostRequestFromRequestCacheConfig.class).autowire();
247247
MvcResult mvcResult = this.mvc.perform(get("/some-url")).andReturn();
248248
RequestCache requestCache = new HttpSessionRequestCache();
249249
String redirectUrl = requestCache.getRequest(mvcResult.getRequest(), mvcResult.getResponse()).getRedirectUrl();
250250
this.mvc
251-
.perform(post("/login").param("username", "user")
252-
.param("password", "password")
253-
.with(csrf())
254-
.session((MockHttpSession) mvcResult.getRequest().getSession()))
255-
.andExpect(status().isFound())
256-
.andExpect(redirectedUrl(redirectUrl));
251+
.perform(post("/login").param("username", "user")
252+
.param("password", "password")
253+
.with(csrf())
254+
.session((MockHttpSession) mvcResult.getRequest().getSession()))
255+
.andExpect(status().isFound())
256+
.andExpect(redirectedUrl(redirectUrl));
257257
verify(CsrfDisablesPostRequestFromRequestCacheConfig.REPO, atLeastOnce())
258-
.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
258+
.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
259259
}
260260

261261
// SEC-2422
262262
@Test
263263
public void postWhenCsrfEnabledAndSessionIsExpiredThenRespondsWithForbidden() throws Exception {
264264
this.spring.register(InvalidSessionUrlConfig.class).autowire();
265265
MvcResult mvcResult = this.mvc.perform(post("/").param("_csrf", "abc"))
266-
.andExpect(status().isFound())
267-
.andExpect(redirectedUrl("/error/sessionError"))
268-
.andReturn();
266+
.andExpect(status().isFound())
267+
.andExpect(redirectedUrl("/error/sessionError"))
268+
.andReturn();
269269
this.mvc.perform(post("/").session((MockHttpSession) mvcResult.getRequest().getSession()))
270-
.andExpect(status().isForbidden());
270+
.andExpect(status().isForbidden());
271271
}
272272

273273
@Test
@@ -306,7 +306,7 @@ public void postWhenCustomCsrfTokenRepositoryThenRepositoryIsUsed() throws Excep
306306
CsrfTokenRepositoryConfig.REPO = mock(CsrfTokenRepository.class);
307307
given(CsrfTokenRepositoryConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
308308
any(HttpServletResponse.class)))
309-
.willReturn(new TestDeferredCsrfToken(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")));
309+
.willReturn(new TestDeferredCsrfToken(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")));
310310
this.spring.register(CsrfTokenRepositoryConfig.class, BasicController.class).autowire();
311311
this.mvc.perform(post("/"));
312312
verify(CsrfTokenRepositoryConfig.REPO).loadDeferredToken(any(HttpServletRequest.class),
@@ -329,7 +329,7 @@ public void loginWhenCustomCsrfTokenRepositoryThenCsrfTokenIsCleared() throws Ex
329329
given(CsrfTokenRepositoryConfig.REPO.loadToken(any())).willReturn(csrfToken);
330330
given(CsrfTokenRepositoryConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
331331
any(HttpServletResponse.class)))
332-
.willReturn(new TestDeferredCsrfToken(csrfToken));
332+
.willReturn(new TestDeferredCsrfToken(csrfToken));
333333
this.spring.register(CsrfTokenRepositoryConfig.class, BasicController.class).autowire();
334334
// @formatter:off
335335
MockHttpServletRequestBuilder loginRequest = post("/login")
@@ -348,7 +348,7 @@ public void getWhenCustomCsrfTokenRepositoryInLambdaThenRepositoryIsUsed() throw
348348
CsrfTokenRepositoryInLambdaConfig.REPO = mock(CsrfTokenRepository.class);
349349
given(CsrfTokenRepositoryInLambdaConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
350350
any(HttpServletResponse.class)))
351-
.willReturn(new TestDeferredCsrfToken(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")));
351+
.willReturn(new TestDeferredCsrfToken(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")));
352352
this.spring.register(CsrfTokenRepositoryInLambdaConfig.class, BasicController.class).autowire();
353353
this.mvc.perform(post("/"));
354354
verify(CsrfTokenRepositoryInLambdaConfig.REPO).loadDeferredToken(any(HttpServletRequest.class),
@@ -418,8 +418,8 @@ public void logoutWhenGetRequestAndGetEnabledForLogoutThenLogsOut() throws Excep
418418
@Test
419419
public void configureWhenRequireCsrfProtectionMatcherNullThenException() {
420420
assertThatExceptionOfType(BeanCreationException.class)
421-
.isThrownBy(() -> this.spring.register(NullRequireCsrfProtectionMatcherConfig.class).autowire())
422-
.withRootCauseInstanceOf(IllegalArgumentException.class);
421+
.isThrownBy(() -> this.spring.register(NullRequireCsrfProtectionMatcherConfig.class).autowire())
422+
.withRootCauseInstanceOf(IllegalArgumentException.class);
423423
}
424424

425425
@Test
@@ -432,8 +432,8 @@ public void getWhenDefaultCsrfTokenRepositoryThenDoesNotCreateSession() throws E
432432
@Test
433433
public void getWhenNullAuthenticationStrategyThenException() {
434434
assertThatExceptionOfType(BeanCreationException.class)
435-
.isThrownBy(() -> this.spring.register(NullAuthenticationStrategy.class).autowire())
436-
.withRootCauseInstanceOf(IllegalArgumentException.class);
435+
.isThrownBy(() -> this.spring.register(NullAuthenticationStrategy.class).autowire())
436+
.withRootCauseInstanceOf(IllegalArgumentException.class);
437437
}
438438

439439
@Test
@@ -456,13 +456,13 @@ public void getLoginWhenCsrfTokenRequestAttributeHandlerSetThenRespondsWithNorma
456456
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
457457
CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
458458
given(csrfTokenRepository.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
459-
.willReturn(new TestDeferredCsrfToken(csrfToken));
459+
.willReturn(new TestDeferredCsrfToken(csrfToken));
460460
CsrfTokenRequestHandlerConfig.REPO = csrfTokenRepository;
461461
CsrfTokenRequestHandlerConfig.HANDLER = new CsrfTokenRequestAttributeHandler();
462462
this.spring.register(CsrfTokenRequestHandlerConfig.class, BasicController.class).autowire();
463463
this.mvc.perform(get("/login"))
464-
.andExpect(status().isOk())
465-
.andExpect(content().string(containsString(csrfToken.getToken())));
464+
.andExpect(status().isOk())
465+
.andExpect(content().string(containsString(csrfToken.getToken())));
466466
verify(csrfTokenRepository).loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
467467
verifyNoMoreInteractions(csrfTokenRepository);
468468
}
@@ -473,7 +473,7 @@ public void loginWhenCsrfTokenRequestAttributeHandlerSetAndNormalCsrfTokenThenSu
473473
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
474474
given(csrfTokenRepository.loadToken(any(HttpServletRequest.class))).willReturn(csrfToken);
475475
given(csrfTokenRepository.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
476-
.willReturn(new TestDeferredCsrfToken(csrfToken));
476+
.willReturn(new TestDeferredCsrfToken(csrfToken));
477477
CsrfTokenRequestHandlerConfig.REPO = csrfTokenRepository;
478478
CsrfTokenRequestHandlerConfig.HANDLER = new CsrfTokenRequestAttributeHandler();
479479
this.spring.register(CsrfTokenRequestHandlerConfig.class, BasicController.class).autowire();
@@ -497,13 +497,13 @@ public void getLoginWhenXorCsrfTokenRequestAttributeHandlerSetThenRespondsWithMa
497497
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
498498
CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
499499
given(csrfTokenRepository.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
500-
.willReturn(new TestDeferredCsrfToken(csrfToken));
500+
.willReturn(new TestDeferredCsrfToken(csrfToken));
501501
CsrfTokenRequestHandlerConfig.REPO = csrfTokenRepository;
502502
CsrfTokenRequestHandlerConfig.HANDLER = new XorCsrfTokenRequestAttributeHandler();
503503
this.spring.register(CsrfTokenRequestHandlerConfig.class, BasicController.class).autowire();
504504
this.mvc.perform(get("/login"))
505-
.andExpect(status().isOk())
506-
.andExpect(content().string(not(containsString(csrfToken.getToken()))));
505+
.andExpect(status().isOk())
506+
.andExpect(content().string(not(containsString(csrfToken.getToken()))));
507507
verify(csrfTokenRepository).loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
508508
verifyNoMoreInteractions(csrfTokenRepository);
509509
}
@@ -514,7 +514,7 @@ public void loginWhenXorCsrfTokenRequestAttributeHandlerSetAndMaskedCsrfTokenThe
514514
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
515515
given(csrfTokenRepository.loadToken(any(HttpServletRequest.class))).willReturn(csrfToken);
516516
given(csrfTokenRepository.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
517-
.willReturn(new TestDeferredCsrfToken(csrfToken));
517+
.willReturn(new TestDeferredCsrfToken(csrfToken));
518518
CsrfTokenRequestHandlerConfig.REPO = csrfTokenRepository;
519519
CsrfTokenRequestHandlerConfig.HANDLER = new XorCsrfTokenRequestAttributeHandler();
520520
this.spring.register(CsrfTokenRequestHandlerConfig.class, BasicController.class).autowire();
@@ -613,35 +613,33 @@ public void getWhenHttpBasicAndCookieCsrfTokenRepositorySetAndNoExistingCookieTh
613613

614614
@Test
615615
public void spaConfigForbidden() throws Exception {
616-
this.spring
617-
.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
618-
.autowire();
616+
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
617+
.autowire();
619618
this.mvc.perform(post("/")).andExpect(status().isForbidden());
620619
}
621620

622621
@Test
623622
public void spaConfigOk() throws Exception {
624-
this.spring
625-
.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
626-
.autowire();
623+
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
624+
.autowire();
627625
this.mvc.perform(post("/").with(csrf())).andExpect(status().isOk());
628626
}
629627

630628
@Test
631629
public void spaConfigDoubleSubmit() throws Exception {
632-
this.spring
633-
.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
634-
.autowire();
635-
var token = this.mvc
636-
.perform(post("/"))
637-
.andExpect(status().isForbidden())
638-
.andExpect(cookie().exists("XSRF-TOKEN"))
639-
.andReturn().getResponse().getCookie("XSRF-TOKEN");
630+
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
631+
.autowire();
632+
var token = this.mvc.perform(post("/"))
633+
.andExpect(status().isForbidden())
634+
.andExpect(cookie().exists("XSRF-TOKEN"))
635+
.andReturn()
636+
.getResponse()
637+
.getCookie("XSRF-TOKEN");
640638

641-
this.mvc.perform(post("/")
642-
.header("X-XSRF-TOKEN", token.getValue())
643-
.cookie(new Cookie("XSRF-TOKEN", token.getValue())))
644-
.andExpect(status().isOk());
639+
this.mvc
640+
.perform(post("/").header("X-XSRF-TOKEN", token.getValue())
641+
.cookie(new Cookie("XSRF-TOKEN", token.getValue())))
642+
.andExpect(status().isOk());
645643
}
646644

647645
@Configuration
@@ -1043,11 +1041,11 @@ static class CsrfSpaConfig {
10431041

10441042
@Bean
10451043
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
1046-
http.csrf(CsrfCustomizer.spaDefaults());
1044+
http.csrf(CsrfConfigurer::spa);
10471045
return http.build();
10481046
}
1049-
}
10501047

1048+
}
10511049

10521050
@Configuration
10531051
@EnableWebSecurity

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

Lines changed: 5 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -787,48 +787,10 @@ public class SecurityConfig {
787787
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
788788
http
789789
// ...
790-
.csrf((csrf) -> csrf
791-
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <1>
792-
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // <2>
793-
);
790+
.csrf((csrf) -> csrf.spa());
794791
return http.build();
795792
}
796793
}
797-
798-
final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
799-
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
800-
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
801-
802-
@Override
803-
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
804-
/*
805-
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
806-
* the CsrfToken when it is rendered in the response body.
807-
*/
808-
this.xor.handle(request, response, csrfToken);
809-
/*
810-
* Render the token value to a cookie by causing the deferred token to be loaded.
811-
*/
812-
csrfToken.get();
813-
}
814-
815-
@Override
816-
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
817-
String headerValue = request.getHeader(csrfToken.getHeaderName());
818-
/*
819-
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
820-
* to resolve the CsrfToken. This applies when a single-page application includes
821-
* the header value automatically, which was obtained via a cookie containing the
822-
* raw CsrfToken.
823-
*
824-
* In all other cases (e.g. if the request contains a request parameter), use
825-
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
826-
* when a server-side rendered form includes the _csrf request parameter as a
827-
* hidden input.
828-
*/
829-
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
830-
}
831-
}
832794
----
833795
834796
Kotlin::
@@ -846,51 +808,12 @@ class SecurityConfig {
846808
http {
847809
// ...
848810
csrf {
849-
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() // <1>
850-
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2>
811+
spa()
851812
}
852813
}
853814
return http.build()
854815
}
855816
}
856-
857-
class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
858-
private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
859-
private val xor: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
860-
861-
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
862-
/*
863-
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
864-
* the CsrfToken when it is rendered in the response body.
865-
*/
866-
xor.handle(request, response, csrfToken)
867-
/*
868-
* Render the token value to a cookie by causing the deferred token to be loaded.
869-
*/
870-
csrfToken.get()
871-
}
872-
873-
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
874-
val headerValue = request.getHeader(csrfToken.headerName)
875-
/*
876-
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
877-
* to resolve the CsrfToken. This applies when a single-page application includes
878-
* the header value automatically, which was obtained via a cookie containing the
879-
* raw CsrfToken.
880-
*/
881-
return if (StringUtils.hasText(headerValue)) {
882-
plain
883-
} else {
884-
/*
885-
* In all other cases (e.g. if the request contains a request parameter), use
886-
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
887-
* when a server-side rendered form includes the _csrf request parameter as a
888-
* hidden input.
889-
*/
890-
xor
891-
}.resolveCsrfTokenValue(request, csrfToken)
892-
}
893-
}
894817
----
895818
896819
XML::
@@ -899,22 +822,13 @@ XML::
899822
----
900823
<http>
901824
<!-- ... -->
902-
<csrf
903-
token-repository-ref="tokenRepository" <1>
904-
request-handler-ref="requestHandler"/> <2>
825+
<csrf>
826+
<spa />
827+
</csrf>
905828
</http>
906-
<b:bean id="tokenRepository"
907-
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
908-
p:cookieHttpOnly="false"/>
909-
<b:bean id="requestHandler"
910-
class="example.SpaCsrfTokenRequestHandler"/>
911829
----
912830
======
913831

914-
<1> Configure `CookieCsrfTokenRepository` with `HttpOnly` set to `false` so the cookie can be read by the JavaScript application.
915-
<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`).
916-
This implementation also causes the deferred `CsrfToken` to be loaded on every request, which will return a new cookie if needed.
917-
918832
[[csrf-integration-javascript-mpa]]
919833
==== Multi-Page Applications
920834

0 commit comments

Comments
 (0)
Please sign in to comment.