Skip to content

Commit 48283ec

Browse files
committed
SEC-2276: Delay saving CsrfToken until token is accessed
This also removed the CsrfToken from the response headers to prevent the token from being saved. If user's wish to return the CsrfToken in the response headers, they should use the CsrfToken found on the request.
1 parent c131fb6 commit 48283ec

File tree

14 files changed

+355
-159
lines changed

14 files changed

+355
-159
lines changed

config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.springframework.security.web.access.intercept.FilterSecurityIntercept
3636
import org.springframework.security.web.context.HttpRequestResponseHolder
3737
import org.springframework.security.web.context.HttpSessionSecurityContextRepository
3838
import org.springframework.security.web.csrf.CsrfToken
39+
import org.springframework.security.web.csrf.DefaultCsrfToken;
3940
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository
4041

4142
import spock.lang.AutoCleanup
@@ -69,7 +70,7 @@ abstract class BaseSpringSpec extends Specification {
6970
}
7071

7172
def setupCsrf(csrfTokenValue="BaseSpringSpec_CSRFTOKEN") {
72-
csrfToken = new CsrfToken("X-CSRF-TOKEN","_csrf",csrfTokenValue)
73+
csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf",csrfTokenValue)
7374
new HttpSessionCsrfTokenRepository().saveToken(csrfToken, request,response)
7475
request.setParameter(csrfToken.parameterName, csrfToken.token)
7576
}

config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ class WebSecurityConfigurerAdapterTests extends BaseSpringSpec {
7979
'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains',
8080
'Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate',
8181
'Pragma':'no-cache',
82-
'X-XSS-Protection' : '1; mode=block',
83-
'X-CSRF-TOKEN' : csrfToken.token]
82+
'X-XSS-Protection' : '1; mode=block']
8483
}
8584

8685
@EnableWebSecurity

config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.groovy

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
4949
'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains',
5050
'Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate',
5151
'Pragma':'no-cache',
52-
'X-XSS-Protection' : '1; mode=block',
53-
'X-CSRF-TOKEN' : csrfToken.token]
52+
'X-XSS-Protection' : '1; mode=block']
5453
}
5554

5655
@Configuration
@@ -70,8 +69,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
7069
springSecurityFilterChain.doFilter(request,response,chain)
7170
then:
7271
responseHeaders == ['Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate',
73-
'Pragma':'no-cache',
74-
'X-CSRF-TOKEN' : csrfToken.token]
72+
'Pragma':'no-cache']
7573
}
7674

7775
@Configuration
@@ -91,8 +89,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
9189
when:
9290
springSecurityFilterChain.doFilter(request,response,chain)
9391
then:
94-
responseHeaders == ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains',
95-
'X-CSRF-TOKEN' : csrfToken.token]
92+
responseHeaders == ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains']
9693
}
9794

9895
@Configuration
@@ -111,8 +108,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
111108
when:
112109
springSecurityFilterChain.doFilter(request,response,chain)
113110
then:
114-
responseHeaders == ['Strict-Transport-Security': 'max-age=15768000',
115-
'X-CSRF-TOKEN' : csrfToken.token]
111+
responseHeaders == ['Strict-Transport-Security': 'max-age=15768000']
116112
}
117113

118114
@Configuration
@@ -133,8 +129,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
133129
when:
134130
springSecurityFilterChain.doFilter(request,response,chain)
135131
then:
136-
responseHeaders == ['X-Frame-Options': 'SAMEORIGIN',
137-
'X-CSRF-TOKEN' : csrfToken.token]
132+
responseHeaders == ['X-Frame-Options': 'SAMEORIGIN']
138133
}
139134

140135
@Configuration
@@ -156,8 +151,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
156151
when:
157152
springSecurityFilterChain.doFilter(request,response,chain)
158153
then:
159-
responseHeaders == ['X-Frame-Options': 'ALLOW-FROM https://example.com',
160-
'X-CSRF-TOKEN' : csrfToken.token]
154+
responseHeaders == ['X-Frame-Options': 'ALLOW-FROM https://example.com']
161155
}
162156

163157

@@ -178,8 +172,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
178172
when:
179173
springSecurityFilterChain.doFilter(request,response,chain)
180174
then:
181-
responseHeaders == ['X-XSS-Protection': '1; mode=block',
182-
'X-CSRF-TOKEN' : csrfToken.token]
175+
responseHeaders == ['X-XSS-Protection': '1; mode=block']
183176
}
184177

185178
@Configuration
@@ -199,8 +192,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
199192
when:
200193
springSecurityFilterChain.doFilter(request,response,chain)
201194
then:
202-
responseHeaders == ['X-XSS-Protection': '1',
203-
'X-CSRF-TOKEN' : csrfToken.token]
195+
responseHeaders == ['X-XSS-Protection': '1']
204196
}
205197

206198
@Configuration
@@ -220,8 +212,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
220212
when:
221213
springSecurityFilterChain.doFilter(request,response,chain)
222214
then:
223-
responseHeaders == ['X-Content-Type-Options': 'nosniff',
224-
'X-CSRF-TOKEN' : csrfToken.token]
215+
responseHeaders == ['X-Content-Type-Options': 'nosniff']
225216
}
226217

227218
@Configuration
@@ -243,8 +234,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
243234
when:
244235
springSecurityFilterChain.doFilter(request,response,chain)
245236
then:
246-
responseHeaders == ['customHeaderName': 'customHeaderValue',
247-
'X-CSRF-TOKEN' : csrfToken.token]
237+
responseHeaders == ['customHeaderName': 'customHeaderValue']
248238
}
249239

250240
@Configuration

config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import org.springframework.security.web.access.AccessDeniedHandler;
2929
import org.springframework.security.web.csrf.CsrfFilter
3030
import org.springframework.security.web.csrf.CsrfToken;
3131
import org.springframework.security.web.csrf.CsrfTokenRepository;
32+
import org.springframework.security.web.csrf.DefaultCsrfToken;
3233
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor
3334
import org.springframework.security.web.util.RequestMatcher
3435

@@ -113,7 +114,7 @@ class CsrfConfigTests extends AbstractHttpConfigTests {
113114
mockBean(CsrfTokenRepository,'repo')
114115
createAppContext()
115116
CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
116-
CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
117+
CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc")
117118
when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
118119
request.setParameter(token.parameterName,token.token)
119120
request.servletPath = "/some-url"
@@ -147,7 +148,7 @@ class CsrfConfigTests extends AbstractHttpConfigTests {
147148
mockBean(CsrfTokenRepository,'repo')
148149
createAppContext()
149150
CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
150-
CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
151+
CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc")
151152
when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
152153
request.setParameter(token.parameterName,token.token)
153154
request.servletPath = "/some-url"
@@ -200,7 +201,7 @@ class CsrfConfigTests extends AbstractHttpConfigTests {
200201
mockBean(CsrfTokenRepository,'repo')
201202
createAppContext()
202203
CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
203-
CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
204+
CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc")
204205
when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
205206
request.setParameter(token.parameterName,token.token)
206207
request.method = "POST"
@@ -223,7 +224,7 @@ class CsrfConfigTests extends AbstractHttpConfigTests {
223224
mockBean(CsrfTokenRepository,'repo')
224225
createAppContext()
225226
CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
226-
CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
227+
CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc")
227228
when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
228229
request.setParameter(token.parameterName,token.token)
229230
request.method = "POST"
@@ -244,7 +245,7 @@ class CsrfConfigTests extends AbstractHttpConfigTests {
244245
mockBean(CsrfTokenRepository,'repo')
245246
createAppContext()
246247
CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
247-
CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
248+
CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc")
248249
when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
249250
request.setParameter(token.parameterName,token.token)
250251
request.method = "POST"

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.springframework.mock.web.MockFilterChain;
4141
import org.springframework.mock.web.MockHttpServletRequest;
4242
import org.springframework.mock.web.MockHttpServletResponse;
43-
import org.springframework.security.authentication.TestingAuthenticationToken;
4443
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
4544
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
4645
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -96,7 +95,9 @@ public void changeSessionIdDefaultsInServlet31Plus() throws Exception {
9695
request.setMethod("POST");
9796
request.setParameter("username", "user");
9897
request.setParameter("password", "password");
99-
CsrfToken token = new HttpSessionCsrfTokenRepository().generateAndSaveToken(request, response);
98+
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
99+
CsrfToken token = repository.generateToken(request);
100+
repository.saveToken(token, request, response);
100101
request.setParameter(token.getParameterName(),token.getToken());
101102
when(ReflectionUtils.findMethod(HttpServletRequest.class, "changeSessionId")).thenReturn(method);
102103

web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ protected void doFilterInternal(HttpServletRequest request,
7070
throws ServletException, IOException {
7171
CsrfToken csrfToken = tokenRepository.loadToken(request);
7272
if(csrfToken == null) {
73-
csrfToken = tokenRepository.generateAndSaveToken(request, response);
73+
CsrfToken generatedToken = tokenRepository.generateToken(request);
74+
csrfToken = new SaveOnAccessCsrfToken(tokenRepository, request, response, generatedToken);
7475
}
7576
request.setAttribute(CsrfToken.class.getName(), csrfToken);
7677
request.setAttribute(csrfToken.getParameterName(), csrfToken);
77-
response.addHeader(csrfToken.getHeaderName(), csrfToken.getToken());
7878

7979
if(!requireCsrfProtectionMatcher.matches(request)) {
8080
filterChain.doFilter(request, response);
@@ -128,7 +128,86 @@ public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
128128
this.accessDeniedHandler = accessDeniedHandler;
129129
}
130130

131-
private static class DefaultRequiresCsrfMatcher implements RequestMatcher {
131+
@SuppressWarnings("serial")
132+
private static final class SaveOnAccessCsrfToken implements CsrfToken {
133+
private transient CsrfTokenRepository tokenRepository;
134+
private transient HttpServletRequest request;
135+
private transient HttpServletResponse response;
136+
137+
private final CsrfToken delegate;
138+
139+
public SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository,
140+
HttpServletRequest request, HttpServletResponse response,
141+
CsrfToken delegate) {
142+
super();
143+
this.tokenRepository = tokenRepository;
144+
this.request = request;
145+
this.response = response;
146+
this.delegate = delegate;
147+
}
148+
149+
public String getHeaderName() {
150+
return delegate.getHeaderName();
151+
}
152+
153+
public String getParameterName() {
154+
return delegate.getParameterName();
155+
}
156+
157+
public String getToken() {
158+
saveTokenIfNecessary();
159+
return delegate.getToken();
160+
}
161+
162+
@Override
163+
public String toString() {
164+
return "SaveOnAccessCsrfToken [delegate=" + delegate + "]";
165+
}
166+
167+
@Override
168+
public int hashCode() {
169+
final int prime = 31;
170+
int result = 1;
171+
result = prime * result
172+
+ ((delegate == null) ? 0 : delegate.hashCode());
173+
return result;
174+
}
175+
176+
@Override
177+
public boolean equals(Object obj) {
178+
if (this == obj)
179+
return true;
180+
if (obj == null)
181+
return false;
182+
if (getClass() != obj.getClass())
183+
return false;
184+
SaveOnAccessCsrfToken other = (SaveOnAccessCsrfToken) obj;
185+
if (delegate == null) {
186+
if (other.delegate != null)
187+
return false;
188+
} else if (!delegate.equals(other.delegate))
189+
return false;
190+
return true;
191+
}
192+
193+
private void saveTokenIfNecessary() {
194+
if(this.tokenRepository == null) {
195+
return;
196+
}
197+
198+
synchronized(this) {
199+
if(tokenRepository != null) {
200+
this.tokenRepository.saveToken(delegate, request, response);
201+
this.tokenRepository = null;
202+
this.request = null;
203+
this.response = null;
204+
}
205+
}
206+
}
207+
208+
}
209+
210+
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
132211
private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
133212

134213
/* (non-Javadoc)

web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,16 @@
1717

1818
import java.io.Serializable;
1919

20-
import org.springframework.util.Assert;
21-
2220
/**
23-
* A CSRF token that is used to protect against CSRF attacks.
21+
* Provides the information about an expected CSRF token.
22+
*
23+
* @see DefaultCsrfToken
2424
*
2525
* @author Rob Winch
2626
* @since 3.2
27+
*
2728
*/
28-
@SuppressWarnings("serial")
29-
public final class CsrfToken implements Serializable {
30-
31-
private final String token;
32-
33-
private final String parameterName;
34-
35-
private final String headerName;
36-
37-
/**
38-
* Creates a new instance
39-
* @param headerName the HTTP header name to use
40-
* @param parameterName the HTTP parameter name to use
41-
* @param token the value of the token (i.e. expected value of the HTTP parameter of parametername).
42-
*/
43-
public CsrfToken(String headerName, String parameterName, String token) {
44-
Assert.hasLength(headerName, "headerName cannot be null or empty");
45-
Assert.hasLength(parameterName, "parameterName cannot be null or empty");
46-
Assert.hasLength(token, "token cannot be null or empty");
47-
this.headerName = headerName;
48-
this.parameterName = parameterName;
49-
this.token = token;
50-
}
29+
public interface CsrfToken extends Serializable {
5130

5231
/**
5332
* Gets the HTTP header that the CSRF is populated on the response and can
@@ -56,23 +35,18 @@ public CsrfToken(String headerName, String parameterName, String token) {
5635
* @return the HTTP header that the CSRF is populated on the response and
5736
* can be placed on requests instead of the parameter
5837
*/
59-
public String getHeaderName() {
60-
return headerName;
61-
}
38+
String getHeaderName();
6239

6340
/**
6441
* Gets the HTTP parameter name that should contain the token. Cannot be null.
6542
* @return the HTTP parameter name that should contain the token.
6643
*/
67-
public String getParameterName() {
68-
return parameterName;
69-
}
44+
String getParameterName();
7045

7146
/**
7247
* Gets the token value. Cannot be null.
7348
* @return the token value
7449
*/
75-
public String getToken() {
76-
return token;
77-
}
78-
}
50+
String getToken();
51+
52+
}

web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRepository.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,14 @@
3333
public interface CsrfTokenRepository {
3434

3535
/**
36-
* Generates and saves the expected {@link CsrfToken}
36+
* Generates a {@link CsrfToken}
3737
*
3838
* @param request
3939
* the {@link HttpServletRequest} to use
40-
* @param response
41-
* the {@link HttpServletResponse} to use
42-
* @return the {@link CsrfToken} that was generated and saved. Cannot be
40+
* @return the {@link CsrfToken} that was generated. Cannot be
4341
* null.
4442
*/
45-
CsrfToken generateAndSaveToken(HttpServletRequest request,
46-
HttpServletResponse response);
43+
CsrfToken generateToken(HttpServletRequest request);
4744

4845
/**
4946
* Saves the {@link CsrfToken} using the {@link HttpServletRequest} and

0 commit comments

Comments
 (0)