Skip to content

Commit c1a6cea

Browse files
committed
Defer CsrfFilter Session Access
Closes gh-11456
2 parents 002a770 + 5b64526 commit c1a6cea

File tree

11 files changed

+185
-1
lines changed

11 files changed

+185
-1
lines changed

Diff for: config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java

+15
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
8989

9090
private SessionAuthenticationStrategy sessionAuthenticationStrategy;
9191

92+
private String csrfRequestAttributeName;
93+
9294
private final ApplicationContext context;
9395

9496
/**
@@ -124,6 +126,16 @@ public CsrfConfigurer<H> requireCsrfProtectionMatcher(RequestMatcher requireCsrf
124126
return this;
125127
}
126128

129+
/**
130+
* Sets the {@link CsrfFilter#setCsrfRequestAttributeName(String)}
131+
* @param csrfRequestAttributeName the attribute name to set the CsrfToken on.
132+
* @return the {@link CsrfConfigurer} for further customizations.
133+
*/
134+
public CsrfConfigurer<H> csrfRequestAttributeName(String csrfRequestAttributeName) {
135+
this.csrfRequestAttributeName = csrfRequestAttributeName;
136+
return this;
137+
}
138+
127139
/**
128140
* <p>
129141
* Allows specifying {@link HttpServletRequest} that should not use CSRF Protection
@@ -202,6 +214,9 @@ public CsrfConfigurer<H> sessionAuthenticationStrategy(
202214
@Override
203215
public void configure(H http) {
204216
CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);
217+
if (this.csrfRequestAttributeName != null) {
218+
filter.setCsrfRequestAttributeName(this.csrfRequestAttributeName);
219+
}
205220
RequestMatcher requireCsrfProtectionMatcher = getRequireCsrfProtectionMatcher();
206221
if (requireCsrfProtectionMatcher != null) {
207222
filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);

Diff for: config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java

+8
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,14 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
6767

6868
private static final String DISPATCHER_SERVLET_CLASS_NAME = "org.springframework.web.servlet.DispatcherServlet";
6969

70+
private static final String ATT_REQUEST_ATTRIBUTE_NAME = "request-attribute-name";
71+
7072
private static final String ATT_MATCHER = "request-matcher-ref";
7173

7274
private static final String ATT_REPOSITORY = "token-repository-ref";
7375

76+
private String requestAttributeName;
77+
7478
private String csrfRepositoryRef;
7579

7680
private BeanDefinition csrfFilter;
@@ -94,6 +98,7 @@ public BeanDefinition parse(Element element, ParserContext pc) {
9498
}
9599
if (element != null) {
96100
this.csrfRepositoryRef = element.getAttribute(ATT_REPOSITORY);
101+
this.requestAttributeName = element.getAttribute(ATT_REQUEST_ATTRIBUTE_NAME);
97102
this.requestMatcherRef = element.getAttribute(ATT_MATCHER);
98103
}
99104
if (!StringUtils.hasText(this.csrfRepositoryRef)) {
@@ -110,6 +115,9 @@ public BeanDefinition parse(Element element, ParserContext pc) {
110115
if (StringUtils.hasText(this.requestMatcherRef)) {
111116
builder.addPropertyReference("requireCsrfProtectionMatcher", this.requestMatcherRef);
112117
}
118+
if (StringUtils.hasText(this.requestAttributeName)) {
119+
builder.addPropertyValue("csrfRequestAttributeName", this.requestAttributeName);
120+
}
113121
this.csrfFilter = builder.getBeanDefinition();
114122
return this.csrfFilter;
115123
}

Diff for: config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc

+3
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,9 @@ csrf =
11361136
csrf-options.attlist &=
11371137
## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled).
11381138
attribute disabled {xsd:boolean}?
1139+
csrf-options.attlist &=
1140+
## The request attribute name the CsrfToken is set on. Default is to set to CsrfToken.parameterName
1141+
attribute request-attribute-name { xsd:token }?
11391142
csrf-options.attlist &=
11401143
## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS"
11411144
attribute request-matcher-ref { xsd:token }?

Diff for: config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd

+7
Original file line numberDiff line numberDiff line change
@@ -3217,6 +3217,13 @@
32173217
</xs:documentation>
32183218
</xs:annotation>
32193219
</xs:attribute>
3220+
<xs:attribute name="request-attribute-name" type="xs:token">
3221+
<xs:annotation>
3222+
<xs:documentation>The request attribute name the CsrfToken is set on. Default is to set to
3223+
CsrfToken.parameterName
3224+
</xs:documentation>
3225+
</xs:annotation>
3226+
</xs:attribute>
32203227
<xs:attribute name="request-matcher-ref" type="xs:token">
32213228
<xs:annotation>
32223229
<xs:documentation>The RequestMatcher instance to be used to determine if CSRF should be applied. Default is

Diff for: config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java

+9
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,15 @@ public void postWhenUsingCsrfAndCustomAccessDeniedHandlerThenTheHandlerIsAppropr
291291
// @formatter:on
292292
}
293293

294+
@Test
295+
public void getWhenUsingCsrfAndCustomRequestAttributeThenSetUsingCsrfAttrName() throws Exception {
296+
this.spring.configLocations(this.xml("WithRequestAttrName")).autowire();
297+
// @formatter:off
298+
MvcResult result = this.mvc.perform(get("/ok")).andReturn();
299+
assertThat(result.getRequest().getAttribute("csrf-attribute-name")).isInstanceOf(CsrfToken.class);
300+
// @formatter:on
301+
}
302+
294303
@Test
295304
public void postWhenHasCsrfTokenButSessionExpiresThenRequestIsCancelledAfterSuccessfulAuthentication()
296305
throws Exception {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2002-2018 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xmlns="http://www.springframework.org/schema/security"
21+
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
22+
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
23+
24+
<http auto-config="true">
25+
<csrf request-attribute-name="csrf-attribute-name"/>
26+
</http>
27+
28+
<b:import resource="CsrfConfigTests-shared-userservice.xml"/>
29+
</b:beans>

Diff for: docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,10 @@ It is highly recommended to leave CSRF protection enabled.
775775
The CsrfTokenRepository to use.
776776
The default is `HttpSessionCsrfTokenRepository`.
777777

778+
[[nsa-csrf-request-attribute-name]]
779+
* **request-attribute-name**
780+
Optional attribute that specifies the request attribute name to set the `CsrfToken` on.
781+
The default is `CsrfToken.parameterName`.
778782

779783
[[nsa-csrf-request-matcher-ref]]
780784
* **request-matcher-ref**

Diff for: web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public final class CsrfFilter extends OncePerRequestFilter {
8787

8888
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
8989

90+
private String csrfRequestAttributeName;
91+
9092
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
9193
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
9294
this.tokenRepository = csrfTokenRepository;
@@ -108,7 +110,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
108110
this.tokenRepository.saveToken(csrfToken, request, response);
109111
}
110112
request.setAttribute(CsrfToken.class.getName(), csrfToken);
111-
request.setAttribute(csrfToken.getParameterName(), csrfToken);
113+
String csrfAttrName = (this.csrfRequestAttributeName != null) ? this.csrfRequestAttributeName
114+
: csrfToken.getParameterName();
115+
request.setAttribute(csrfAttrName, csrfToken);
112116
if (!this.requireCsrfProtectionMatcher.matches(request)) {
113117
if (this.logger.isTraceEnabled()) {
114118
this.logger.trace("Did not protect against CSRF since request did not match "
@@ -167,6 +171,18 @@ public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
167171
this.accessDeniedHandler = accessDeniedHandler;
168172
}
169173

174+
/**
175+
* The {@link CsrfToken} is available as a request attribute named
176+
* {@code CsrfToken.class.getName()}. By default, an additional request attribute that
177+
* is the same as {@link CsrfToken#getParameterName()} is set. This attribute allows
178+
* overriding the additional attribute.
179+
* @param csrfRequestAttributeName the name of an additional request attribute with
180+
* the value of the CsrfToken. Default is {@link CsrfToken#getParameterName()}
181+
*/
182+
public void setCsrfRequestAttributeName(String csrfRequestAttributeName) {
183+
this.csrfRequestAttributeName = csrfRequestAttributeName;
184+
}
185+
170186
/**
171187
* Constant time comparison to prevent against timing attacks.
172188
* @param expected

Diff for: web/src/main/java/org/springframework/security/web/csrf/LazyCsrfTokenRepository.java

+63
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public final class LazyCsrfTokenRepository implements CsrfTokenRepository {
3838

3939
private final CsrfTokenRepository delegate;
4040

41+
private boolean deferLoadToken;
42+
4143
/**
4244
* Creates a new instance
4345
* @param delegate the {@link CsrfTokenRepository} to use. Cannot be null
@@ -48,6 +50,15 @@ public LazyCsrfTokenRepository(CsrfTokenRepository delegate) {
4850
this.delegate = delegate;
4951
}
5052

53+
/**
54+
* Determines if {@link #loadToken(HttpServletRequest)} should be lazily loaded.
55+
* @param deferLoadToken true if should lazily load
56+
* {@link #loadToken(HttpServletRequest)}. Default false.
57+
*/
58+
public void setDeferLoadToken(boolean deferLoadToken) {
59+
this.deferLoadToken = deferLoadToken;
60+
}
61+
5162
/**
5263
* Generates a new token
5364
* @param request the {@link HttpServletRequest} to use. The
@@ -77,6 +88,9 @@ public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletRe
7788
*/
7889
@Override
7990
public CsrfToken loadToken(HttpServletRequest request) {
91+
if (this.deferLoadToken) {
92+
return new LazyLoadCsrfToken(request, this.delegate);
93+
}
8094
return this.delegate.loadToken(request);
8195
}
8296

@@ -92,6 +106,55 @@ private HttpServletResponse getResponse(HttpServletRequest request) {
92106
return response;
93107
}
94108

109+
private final class LazyLoadCsrfToken implements CsrfToken {
110+
111+
private final HttpServletRequest request;
112+
113+
private final CsrfTokenRepository tokenRepository;
114+
115+
private CsrfToken token;
116+
117+
private LazyLoadCsrfToken(HttpServletRequest request, CsrfTokenRepository tokenRepository) {
118+
this.request = request;
119+
this.tokenRepository = tokenRepository;
120+
}
121+
122+
private CsrfToken getDelegate() {
123+
if (this.token != null) {
124+
return this.token;
125+
}
126+
// load from the delegate repository
127+
this.token = LazyCsrfTokenRepository.this.delegate.loadToken(this.request);
128+
if (this.token == null) {
129+
// return a generated token that is lazily saved since
130+
// LazyCsrfTokenRepository#loadToken always returns a value
131+
this.token = generateToken(this.request);
132+
}
133+
return this.token;
134+
}
135+
136+
@Override
137+
public String getHeaderName() {
138+
return getDelegate().getHeaderName();
139+
}
140+
141+
@Override
142+
public String getParameterName() {
143+
return getDelegate().getParameterName();
144+
}
145+
146+
@Override
147+
public String getToken() {
148+
return getDelegate().getToken();
149+
}
150+
151+
@Override
152+
public String toString() {
153+
return "LazyLoadCsrfToken{" + "token=" + this.token + '}';
154+
}
155+
156+
}
157+
95158
private static final class SaveOnAccessCsrfToken implements CsrfToken {
96159

97160
private transient CsrfTokenRepository tokenRepository;

Diff for: web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java

+18
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import static org.mockito.Mockito.never;
4949
import static org.mockito.Mockito.times;
5050
import static org.mockito.Mockito.verify;
51+
import static org.mockito.Mockito.verifyNoInteractions;
5152
import static org.mockito.Mockito.verifyZeroInteractions;
5253

5354
/**
@@ -344,6 +345,23 @@ public void setAccessDeniedHandlerNull() {
344345
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAccessDeniedHandler(null));
345346
}
346347

348+
// This ensures that the HttpSession on get requests unless the CsrfToken is used
349+
@Test
350+
public void doFilterWhenCsrfRequestAttributeNameThenNoCsrfTokenMethodInvokedOnGet()
351+
throws ServletException, IOException {
352+
CsrfFilter filter = createCsrfFilter(this.tokenRepository);
353+
String csrfAttrName = "_csrf";
354+
filter.setCsrfRequestAttributeName(csrfAttrName);
355+
CsrfToken expectedCsrfToken = mock(CsrfToken.class);
356+
given(this.tokenRepository.loadToken(this.request)).willReturn(expectedCsrfToken);
357+
358+
filter.doFilter(this.request, this.response, this.filterChain);
359+
360+
verifyNoInteractions(expectedCsrfToken);
361+
CsrfToken tokenFromRequest = (CsrfToken) this.request.getAttribute(csrfAttrName);
362+
assertThat(tokenFromRequest).isEqualTo(expectedCsrfToken);
363+
}
364+
347365
private static CsrfTokenAssert assertToken(Object token) {
348366
return new CsrfTokenAssert((CsrfToken) token);
349367
}

Diff for: web/src/test/java/org/springframework/security/web/csrf/LazyCsrfTokenRepositoryTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import static org.mockito.BDDMockito.given;
3232
import static org.mockito.Mockito.mock;
3333
import static org.mockito.Mockito.verify;
34+
import static org.mockito.Mockito.verifyNoInteractions;
3435
import static org.mockito.Mockito.verifyZeroInteractions;
3536

3637
/**
@@ -98,4 +99,15 @@ public void loadTokenDelegates() {
9899
verify(this.delegate).loadToken(this.request);
99100
}
100101

102+
@Test
103+
public void loadTokenWhenDeferLoadToken() {
104+
given(this.delegate.loadToken(this.request)).willReturn(this.token);
105+
this.repository.setDeferLoadToken(true);
106+
CsrfToken loadToken = this.repository.loadToken(this.request);
107+
verifyNoInteractions(this.delegate);
108+
assertThat(loadToken.getToken()).isEqualTo(this.token.getToken());
109+
assertThat(loadToken.getHeaderName()).isEqualTo(this.token.getHeaderName());
110+
assertThat(loadToken.getParameterName()).isEqualTo(this.token.getParameterName());
111+
}
112+
101113
}

0 commit comments

Comments
 (0)