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 ec53749

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

File tree

4 files changed

+190
-272
lines changed

4 files changed

+190
-272
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: 125 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@
9292
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
9393
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
9494
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
95-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
95+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
96+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
97+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
98+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
9699

97100
/**
98101
* Tests for {@link CsrfConfigurer}
@@ -114,72 +117,72 @@ public class CsrfConfigurerTests {
114117
@Test
115118
public void postWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
116119
this.spring
117-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
118-
.autowire();
120+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
121+
.autowire();
119122
this.mvc.perform(post("/")).andExpect(status().isForbidden());
120123
}
121124

122125
@Test
123126
public void putWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
124127
this.spring
125-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
126-
.autowire();
128+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
129+
.autowire();
127130
this.mvc.perform(put("/")).andExpect(status().isForbidden());
128131
}
129132

130133
@Test
131134
public void patchWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
132135
this.spring
133-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
134-
.autowire();
136+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
137+
.autowire();
135138
this.mvc.perform(patch("/")).andExpect(status().isForbidden());
136139
}
137140

138141
@Test
139142
public void deleteWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
140143
this.spring
141-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
142-
.autowire();
144+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
145+
.autowire();
143146
this.mvc.perform(delete("/")).andExpect(status().isForbidden());
144147
}
145148

146149
@Test
147150
public void invalidWhenWebSecurityEnabledThenRespondsWithForbidden() throws Exception {
148151
this.spring
149-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
150-
.autowire();
152+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
153+
.autowire();
151154
this.mvc.perform(request("INVALID", URI.create("/"))).andExpect(status().isForbidden());
152155
}
153156

154157
@Test
155158
public void getWhenWebSecurityEnabledThenRespondsWithOk() throws Exception {
156159
this.spring
157-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
158-
.autowire();
160+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
161+
.autowire();
159162
this.mvc.perform(get("/")).andExpect(status().isOk());
160163
}
161164

162165
@Test
163166
public void headWhenWebSecurityEnabledThenRespondsWithOk() throws Exception {
164167
this.spring
165-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
166-
.autowire();
168+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
169+
.autowire();
167170
this.mvc.perform(head("/")).andExpect(status().isOk());
168171
}
169172

170173
@Test
171174
public void traceWhenWebSecurityEnabledThenRespondsWithOk() throws Exception {
172175
this.spring
173-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
174-
.autowire();
176+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
177+
.autowire();
175178
this.mvc.perform(request(HttpMethod.TRACE, "/")).andExpect(status().isOk());
176179
}
177180

178181
@Test
179182
public void optionsWhenWebSecurityEnabledThenRespondsWithOk() throws Exception {
180183
this.spring
181-
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
182-
.autowire();
184+
.register(CsrfAppliedDefaultConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
185+
.autowire();
183186
this.mvc.perform(options("/")).andExpect(status().isOk());
184187
}
185188

@@ -209,11 +212,11 @@ public void loginWhenCsrfDisabledThenRedirectsToPreviousPostRequest() throws Exc
209212
RequestCache requestCache = new HttpSessionRequestCache();
210213
String redirectUrl = requestCache.getRequest(mvcResult.getRequest(), mvcResult.getResponse()).getRedirectUrl();
211214
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));
215+
.perform(post("/login").param("username", "user")
216+
.param("password", "password")
217+
.session((MockHttpSession) mvcResult.getRequest().getSession()))
218+
.andExpect(status().isFound())
219+
.andExpect(redirectedUrl(redirectUrl));
217220
}
218221

219222
@Test
@@ -222,18 +225,18 @@ public void loginWhenCsrfEnabledThenDoesNotRedirectToPreviousPostRequest() throw
222225
DefaultCsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
223226
given(CsrfDisablesPostRequestFromRequestCacheConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
224227
any(HttpServletResponse.class)))
225-
.willReturn(new TestDeferredCsrfToken(csrfToken));
228+
.willReturn(new TestDeferredCsrfToken(csrfToken));
226229
this.spring.register(CsrfDisablesPostRequestFromRequestCacheConfig.class).autowire();
227230
MvcResult mvcResult = this.mvc.perform(post("/some-url")).andReturn();
228231
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("/"));
232+
.perform(post("/login").param("username", "user")
233+
.param("password", "password")
234+
.with(csrf())
235+
.session((MockHttpSession) mvcResult.getRequest().getSession()))
236+
.andExpect(status().isFound())
237+
.andExpect(redirectedUrl("/"));
235238
verify(CsrfDisablesPostRequestFromRequestCacheConfig.REPO, atLeastOnce())
236-
.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
239+
.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
237240
}
238241

239242
@Test
@@ -242,32 +245,32 @@ public void loginWhenCsrfEnabledThenRedirectsToPreviousGetRequest() throws Excep
242245
DefaultCsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
243246
given(CsrfDisablesPostRequestFromRequestCacheConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
244247
any(HttpServletResponse.class)))
245-
.willReturn(new TestDeferredCsrfToken(csrfToken));
248+
.willReturn(new TestDeferredCsrfToken(csrfToken));
246249
this.spring.register(CsrfDisablesPostRequestFromRequestCacheConfig.class).autowire();
247250
MvcResult mvcResult = this.mvc.perform(get("/some-url")).andReturn();
248251
RequestCache requestCache = new HttpSessionRequestCache();
249252
String redirectUrl = requestCache.getRequest(mvcResult.getRequest(), mvcResult.getResponse()).getRedirectUrl();
250253
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));
254+
.perform(post("/login").param("username", "user")
255+
.param("password", "password")
256+
.with(csrf())
257+
.session((MockHttpSession) mvcResult.getRequest().getSession()))
258+
.andExpect(status().isFound())
259+
.andExpect(redirectedUrl(redirectUrl));
257260
verify(CsrfDisablesPostRequestFromRequestCacheConfig.REPO, atLeastOnce())
258-
.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
261+
.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
259262
}
260263

261264
// SEC-2422
262265
@Test
263266
public void postWhenCsrfEnabledAndSessionIsExpiredThenRespondsWithForbidden() throws Exception {
264267
this.spring.register(InvalidSessionUrlConfig.class).autowire();
265268
MvcResult mvcResult = this.mvc.perform(post("/").param("_csrf", "abc"))
266-
.andExpect(status().isFound())
267-
.andExpect(redirectedUrl("/error/sessionError"))
268-
.andReturn();
269+
.andExpect(status().isFound())
270+
.andExpect(redirectedUrl("/error/sessionError"))
271+
.andReturn();
269272
this.mvc.perform(post("/").session((MockHttpSession) mvcResult.getRequest().getSession()))
270-
.andExpect(status().isForbidden());
273+
.andExpect(status().isForbidden());
271274
}
272275

273276
@Test
@@ -306,7 +309,7 @@ public void postWhenCustomCsrfTokenRepositoryThenRepositoryIsUsed() throws Excep
306309
CsrfTokenRepositoryConfig.REPO = mock(CsrfTokenRepository.class);
307310
given(CsrfTokenRepositoryConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
308311
any(HttpServletResponse.class)))
309-
.willReturn(new TestDeferredCsrfToken(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")));
312+
.willReturn(new TestDeferredCsrfToken(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")));
310313
this.spring.register(CsrfTokenRepositoryConfig.class, BasicController.class).autowire();
311314
this.mvc.perform(post("/"));
312315
verify(CsrfTokenRepositoryConfig.REPO).loadDeferredToken(any(HttpServletRequest.class),
@@ -329,7 +332,7 @@ public void loginWhenCustomCsrfTokenRepositoryThenCsrfTokenIsCleared() throws Ex
329332
given(CsrfTokenRepositoryConfig.REPO.loadToken(any())).willReturn(csrfToken);
330333
given(CsrfTokenRepositoryConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
331334
any(HttpServletResponse.class)))
332-
.willReturn(new TestDeferredCsrfToken(csrfToken));
335+
.willReturn(new TestDeferredCsrfToken(csrfToken));
333336
this.spring.register(CsrfTokenRepositoryConfig.class, BasicController.class).autowire();
334337
// @formatter:off
335338
MockHttpServletRequestBuilder loginRequest = post("/login")
@@ -348,7 +351,7 @@ public void getWhenCustomCsrfTokenRepositoryInLambdaThenRepositoryIsUsed() throw
348351
CsrfTokenRepositoryInLambdaConfig.REPO = mock(CsrfTokenRepository.class);
349352
given(CsrfTokenRepositoryInLambdaConfig.REPO.loadDeferredToken(any(HttpServletRequest.class),
350353
any(HttpServletResponse.class)))
351-
.willReturn(new TestDeferredCsrfToken(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")));
354+
.willReturn(new TestDeferredCsrfToken(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")));
352355
this.spring.register(CsrfTokenRepositoryInLambdaConfig.class, BasicController.class).autowire();
353356
this.mvc.perform(post("/"));
354357
verify(CsrfTokenRepositoryInLambdaConfig.REPO).loadDeferredToken(any(HttpServletRequest.class),
@@ -418,8 +421,8 @@ public void logoutWhenGetRequestAndGetEnabledForLogoutThenLogsOut() throws Excep
418421
@Test
419422
public void configureWhenRequireCsrfProtectionMatcherNullThenException() {
420423
assertThatExceptionOfType(BeanCreationException.class)
421-
.isThrownBy(() -> this.spring.register(NullRequireCsrfProtectionMatcherConfig.class).autowire())
422-
.withRootCauseInstanceOf(IllegalArgumentException.class);
424+
.isThrownBy(() -> this.spring.register(NullRequireCsrfProtectionMatcherConfig.class).autowire())
425+
.withRootCauseInstanceOf(IllegalArgumentException.class);
423426
}
424427

425428
@Test
@@ -432,8 +435,8 @@ public void getWhenDefaultCsrfTokenRepositoryThenDoesNotCreateSession() throws E
432435
@Test
433436
public void getWhenNullAuthenticationStrategyThenException() {
434437
assertThatExceptionOfType(BeanCreationException.class)
435-
.isThrownBy(() -> this.spring.register(NullAuthenticationStrategy.class).autowire())
436-
.withRootCauseInstanceOf(IllegalArgumentException.class);
438+
.isThrownBy(() -> this.spring.register(NullAuthenticationStrategy.class).autowire())
439+
.withRootCauseInstanceOf(IllegalArgumentException.class);
437440
}
438441

439442
@Test
@@ -456,13 +459,13 @@ public void getLoginWhenCsrfTokenRequestAttributeHandlerSetThenRespondsWithNorma
456459
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
457460
CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
458461
given(csrfTokenRepository.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
459-
.willReturn(new TestDeferredCsrfToken(csrfToken));
462+
.willReturn(new TestDeferredCsrfToken(csrfToken));
460463
CsrfTokenRequestHandlerConfig.REPO = csrfTokenRepository;
461464
CsrfTokenRequestHandlerConfig.HANDLER = new CsrfTokenRequestAttributeHandler();
462465
this.spring.register(CsrfTokenRequestHandlerConfig.class, BasicController.class).autowire();
463466
this.mvc.perform(get("/login"))
464-
.andExpect(status().isOk())
465-
.andExpect(content().string(containsString(csrfToken.getToken())));
467+
.andExpect(status().isOk())
468+
.andExpect(content().string(containsString(csrfToken.getToken())));
466469
verify(csrfTokenRepository).loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
467470
verifyNoMoreInteractions(csrfTokenRepository);
468471
}
@@ -473,7 +476,7 @@ public void loginWhenCsrfTokenRequestAttributeHandlerSetAndNormalCsrfTokenThenSu
473476
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
474477
given(csrfTokenRepository.loadToken(any(HttpServletRequest.class))).willReturn(csrfToken);
475478
given(csrfTokenRepository.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
476-
.willReturn(new TestDeferredCsrfToken(csrfToken));
479+
.willReturn(new TestDeferredCsrfToken(csrfToken));
477480
CsrfTokenRequestHandlerConfig.REPO = csrfTokenRepository;
478481
CsrfTokenRequestHandlerConfig.HANDLER = new CsrfTokenRequestAttributeHandler();
479482
this.spring.register(CsrfTokenRequestHandlerConfig.class, BasicController.class).autowire();
@@ -497,13 +500,13 @@ public void getLoginWhenXorCsrfTokenRequestAttributeHandlerSetThenRespondsWithMa
497500
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
498501
CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
499502
given(csrfTokenRepository.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
500-
.willReturn(new TestDeferredCsrfToken(csrfToken));
503+
.willReturn(new TestDeferredCsrfToken(csrfToken));
501504
CsrfTokenRequestHandlerConfig.REPO = csrfTokenRepository;
502505
CsrfTokenRequestHandlerConfig.HANDLER = new XorCsrfTokenRequestAttributeHandler();
503506
this.spring.register(CsrfTokenRequestHandlerConfig.class, BasicController.class).autowire();
504507
this.mvc.perform(get("/login"))
505-
.andExpect(status().isOk())
506-
.andExpect(content().string(not(containsString(csrfToken.getToken()))));
508+
.andExpect(status().isOk())
509+
.andExpect(content().string(not(containsString(csrfToken.getToken()))));
507510
verify(csrfTokenRepository).loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
508511
verifyNoMoreInteractions(csrfTokenRepository);
509512
}
@@ -514,7 +517,7 @@ public void loginWhenXorCsrfTokenRequestAttributeHandlerSetAndMaskedCsrfTokenThe
514517
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
515518
given(csrfTokenRepository.loadToken(any(HttpServletRequest.class))).willReturn(csrfToken);
516519
given(csrfTokenRepository.loadDeferredToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
517-
.willReturn(new TestDeferredCsrfToken(csrfToken));
520+
.willReturn(new TestDeferredCsrfToken(csrfToken));
518521
CsrfTokenRequestHandlerConfig.REPO = csrfTokenRepository;
519522
CsrfTokenRequestHandlerConfig.HANDLER = new XorCsrfTokenRequestAttributeHandler();
520523
this.spring.register(CsrfTokenRequestHandlerConfig.class, BasicController.class).autowire();
@@ -576,8 +579,8 @@ public void postWhenHttpBasicAndCookieCsrfTokenRepositorySetAndExistingTokenThen
576579
headers.setBasicAuth("user", "password");
577580
// @formatter:off
578581
MvcResult mvcResult = this.mvc.perform(post("/")
579-
.cookie(existingCookie)
580-
.headers(headers))
582+
.cookie(existingCookie)
583+
.headers(headers))
581584
.andExpect(status().isOk())
582585
.andReturn();
583586
// @formatter:on
@@ -602,7 +605,7 @@ public void getWhenHttpBasicAndCookieCsrfTokenRepositorySetAndNoExistingCookieTh
602605
headers.setBasicAuth("user", "password");
603606
// @formatter:off
604607
MvcResult mvcResult = this.mvc.perform(get("/")
605-
.headers(headers))
608+
.headers(headers))
606609
.andExpect(status().isOk())
607610
.andReturn();
608611
// @formatter:on
@@ -613,35 +616,33 @@ public void getWhenHttpBasicAndCookieCsrfTokenRepositorySetAndNoExistingCookieTh
613616

614617
@Test
615618
public void spaConfigForbidden() throws Exception {
616-
this.spring
617-
.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
618-
.autowire();
619+
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
620+
.autowire();
619621
this.mvc.perform(post("/")).andExpect(status().isForbidden());
620622
}
621623

622624
@Test
623625
public void spaConfigOk() throws Exception {
624-
this.spring
625-
.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
626-
.autowire();
626+
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
627+
.autowire();
627628
this.mvc.perform(post("/").with(csrf())).andExpect(status().isOk());
628629
}
629630

630631
@Test
631632
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");
633+
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
634+
.autowire();
635+
var token = this.mvc.perform(post("/"))
636+
.andExpect(status().isForbidden())
637+
.andExpect(cookie().exists("XSRF-TOKEN"))
638+
.andReturn()
639+
.getResponse()
640+
.getCookie("XSRF-TOKEN");
640641

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

647648
@Configuration
@@ -675,7 +676,7 @@ static class DisableCsrfConfig {
675676
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
676677
// @formatter:off
677678
http
678-
.csrf()
679+
.csrf()
679680
.disable();
680681
return http.build();
681682
// @formatter:on
@@ -691,7 +692,7 @@ static class DisableCsrfInLambdaConfig {
691692
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
692693
// @formatter:off
693694
http
694-
.csrf(AbstractHttpConfigurer::disable);
695+
.csrf(AbstractHttpConfigurer::disable);
695696
return http.build();
696697
// @formatter:on
697698
}
@@ -706,12 +707,12 @@ static class DisableCsrfEnablesRequestCacheConfig {
706707
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
707708
// @formatter:off
708709
http
709-
.authorizeRequests()
710+
.authorizeRequests()
710711
.anyRequest().authenticated()
711712
.and()
712-
.formLogin()
713+
.formLogin()
713714
.and()
714-
.csrf()
715+
.csrf()
715716
.disable();
716717
// @formatter:on
717718
return http.build();
@@ -734,12 +735,12 @@ static class CsrfDisablesPostRequestFromRequestCacheConfig {
734735
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
735736
// @formatter:off
736737
http
737-
.authorizeRequests()
738+
.authorizeRequests()
738739
.anyRequest().authenticated()
739740
.and()
740-
.formLogin()
741+
.formLogin()
741742
.and()
742-
.csrf()
743+
.csrf()
743744
.csrfTokenRepository(REPO);
744745
// @formatter:on
745746
return http.build();
@@ -760,9 +761,9 @@ static class InvalidSessionUrlConfig {
760761
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
761762
// @formatter:off
762763
http
763-
.csrf()
764+
.csrf()
764765
.and()
765-
.sessionManagement()
766+
.sessionManagement()
766767
.invalidSessionUrl("/error/sessionError");
767768
return http.build();
768769
// @formatter:on
@@ -780,7 +781,7 @@ static class RequireCsrfProtectionMatcherConfig {
780781
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
781782
// @formatter:off
782783
http
783-
.csrf()
784+
.csrf()
784785
.requireCsrfProtectionMatcher(MATCHER);
785786
return http.build();
786787
// @formatter:on
@@ -798,7 +799,7 @@ static class RequireCsrfProtectionMatcherInLambdaConfig {
798799
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
799800
// @formatter:off
800801
http
801-
.csrf((csrf) -> csrf.requireCsrfProtectionMatcher(MATCHER));
802+
.csrf((csrf) -> csrf.requireCsrfProtectionMatcher(MATCHER));
802803
return http.build();
803804
// @formatter:on
804805
}
@@ -815,9 +816,9 @@ static class CsrfTokenRepositoryConfig {
815816
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
816817
// @formatter:off
817818
http
818-
.formLogin()
819+
.formLogin()
819820
.and()
820-
.csrf()
821+
.csrf()
821822
.csrfTokenRepository(REPO);
822823
// @formatter:on
823824
return http.build();
@@ -840,8 +841,8 @@ static class CsrfTokenRepositoryInLambdaConfig {
840841
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
841842
// @formatter:off
842843
http
843-
.formLogin(withDefaults())
844-
.csrf((csrf) -> csrf.csrfTokenRepository(REPO));
844+
.formLogin(withDefaults())
845+
.csrf((csrf) -> csrf.csrfTokenRepository(REPO));
845846
return http.build();
846847
// @formatter:on
847848
}
@@ -858,7 +859,7 @@ static class AccessDeniedHandlerConfig {
858859
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
859860
// @formatter:off
860861
http
861-
.exceptionHandling()
862+
.exceptionHandling()
862863
.accessDeniedHandler(DENIED_HANDLER);
863864
return http.build();
864865
// @formatter:on
@@ -878,7 +879,7 @@ static class DefaultAccessDeniedHandlerForConfig {
878879
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
879880
// @formatter:off
880881
http
881-
.exceptionHandling()
882+
.exceptionHandling()
882883
.defaultAccessDeniedHandlerFor(DENIED_HANDLER, MATCHER);
883884
return http.build();
884885
// @formatter:on
@@ -894,7 +895,7 @@ static class FormLoginConfig {
894895
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
895896
// @formatter:off
896897
http
897-
.formLogin();
898+
.formLogin();
898899
return http.build();
899900
// @formatter:on
900901
}
@@ -909,9 +910,9 @@ static class LogoutAllowsGetConfig {
909910
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
910911
// @formatter:off
911912
http
912-
.formLogin()
913+
.formLogin()
913914
.and()
914-
.logout()
915+
.logout()
915916
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
916917
return http.build();
917918
// @formatter:on
@@ -927,7 +928,7 @@ static class NullRequireCsrfProtectionMatcherConfig {
927928
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
928929
// @formatter:off
929930
http
930-
.csrf()
931+
.csrf()
931932
.requireCsrfProtectionMatcher(null);
932933
return http.build();
933934
// @formatter:on
@@ -943,12 +944,12 @@ static class DefaultDoesNotCreateSession {
943944
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
944945
// @formatter:off
945946
http
946-
.authorizeRequests()
947+
.authorizeRequests()
947948
.anyRequest().permitAll()
948949
.and()
949-
.formLogin()
950+
.formLogin()
950951
.and()
951-
.httpBasic();
952+
.httpBasic();
952953
// @formatter:on
953954
return http.build();
954955
}
@@ -1013,14 +1014,14 @@ static class CsrfTokenRequestHandlerConfig {
10131014
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
10141015
// @formatter:off
10151016
http
1016-
.authorizeHttpRequests((authorize) -> authorize
1017-
.anyRequest().authenticated()
1018-
)
1019-
.formLogin(Customizer.withDefaults())
1020-
.csrf((csrf) -> csrf
1021-
.csrfTokenRepository(REPO)
1022-
.csrfTokenRequestHandler(HANDLER)
1023-
);
1017+
.authorizeHttpRequests((authorize) -> authorize
1018+
.anyRequest().authenticated()
1019+
)
1020+
.formLogin(Customizer.withDefaults())
1021+
.csrf((csrf) -> csrf
1022+
.csrfTokenRepository(REPO)
1023+
.csrfTokenRequestHandler(HANDLER)
1024+
);
10241025
// @formatter:on
10251026

10261027
return http.build();
@@ -1043,11 +1044,11 @@ static class CsrfSpaConfig {
10431044

10441045
@Bean
10451046
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
1046-
http.csrf(CsrfCustomizer.spaDefaults());
1047+
http.csrf(CsrfConfigurer::spa);
10471048
return http.build();
10481049
}
1049-
}
10501050

1051+
}
10511052

10521053
@Configuration
10531054
@EnableWebSecurity
@@ -1061,14 +1062,14 @@ static class HttpBasicCsrfTokenRequestHandlerConfig {
10611062
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
10621063
// @formatter:off
10631064
http
1064-
.authorizeHttpRequests((authorize) -> authorize
1065-
.anyRequest().authenticated()
1066-
)
1067-
.httpBasic(Customizer.withDefaults())
1068-
.csrf((csrf) -> csrf
1069-
.csrfTokenRepository(REPO)
1070-
.csrfTokenRequestHandler(HANDLER)
1071-
);
1065+
.authorizeHttpRequests((authorize) -> authorize
1066+
.anyRequest().authenticated()
1067+
)
1068+
.httpBasic(Customizer.withDefaults())
1069+
.csrf((csrf) -> csrf
1070+
.csrfTokenRepository(REPO)
1071+
.csrfTokenRequestHandler(HANDLER)
1072+
);
10721073
// @formatter:on
10731074

10741075
return http.build();
@@ -1078,7 +1079,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
10781079
void configure(AuthenticationManagerBuilder auth) throws Exception {
10791080
// @formatter:off
10801081
auth
1081-
.inMemoryAuthentication()
1082+
.inMemoryAuthentication()
10821083
.withUser(PasswordEncodedUser.user());
10831084
// @formatter:on
10841085
}

‎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.