From a2c0c2958c14f9002f1e3c6b044b4810e665ee71 Mon Sep 17 00:00:00 2001 From: Ruby Hartono <58564005+rh-id@users.noreply.github.com> Date: Thu, 5 Mar 2020 12:43:17 +0700 Subject: [PATCH] Implement new XorCsrfToken for BREACH attack protection 1. added new XorCsrfToken class 2. introduce new method setGenerateToken for CookieCsrfTokenRepository and HttpSessionCsrfTokenRepository to customize CsrfToken implementation 3. deprecate `setHeaderName` and `setParameterName` Fixes gh-4001 Co-Authored-By: Rob Winch --- .../security/config/http/CsrfConfigTests.java | 4 +- .../_includes/about/exploits/csrf.adoc | 7 + .../_includes/servlet/exploits/csrf.adoc | 59 ++++++ .../web/csrf/CsrfChannelInterceptor.java | 4 +- ...yMockMvcRequestBuildersFormLoginTests.java | 11 +- ...MockMvcRequestBuildersFormLogoutTests.java | 11 +- .../web/csrf/CookieCsrfTokenRepository.java | 33 ++- .../security/web/csrf/CsrfFilter.java | 4 +- .../security/web/csrf/CsrfToken.java | 13 +- .../web/csrf/GenerateTokenProvider.java | 38 ++++ .../csrf/HttpSessionCsrfTokenRepository.java | 31 ++- .../web/csrf/LazyCsrfTokenRepository.java | 8 +- .../security/web/csrf/XorCsrfToken.java | 188 ++++++++++++++++++ .../csrf/CookieCsrfTokenRepositoryTests.java | 80 +++++++- .../csrf/CsrfAuthenticationStrategyTests.java | 4 +- .../security/web/csrf/CsrfFilterTests.java | 4 +- .../web/csrf/DefaultCsrfTokenTests.java | 30 ++- .../HttpSessionCsrfTokenRepositoryTests.java | 71 ++++++- .../security/web/csrf/XorCsrfTokenTests.java | 100 ++++++++++ .../jackson2/DefaultCsrfTokenMixinTests.java | 5 +- .../CsrfRequestDataValueProcessorTests.java | 18 +- 21 files changed, 668 insertions(+), 55 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/csrf/GenerateTokenProvider.java create mode 100644 web/src/main/java/org/springframework/security/web/csrf/XorCsrfToken.java create mode 100644 web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenTests.java diff --git a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java index fa4b41b400f..5030007f1fd 100644 --- a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -640,7 +640,7 @@ public void match(MvcResult result) throws Exception { MockHttpServletRequest request = result.getRequest(); CsrfToken token = WebTestUtils.getCsrfTokenRepository(request).loadToken(request); assertThat(token).isNotNull(); - assertThat(token.getToken()).isEqualTo(this.token.apply(result)); + assertThat(token.matches(this.token.apply(result))).isTrue(); } } diff --git a/docs/manual/src/docs/asciidoc/_includes/about/exploits/csrf.adoc b/docs/manual/src/docs/asciidoc/_includes/about/exploits/csrf.adoc index 647aaa6a8e4..7b80f1861dd 100644 --- a/docs/manual/src/docs/asciidoc/_includes/about/exploits/csrf.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/about/exploits/csrf.adoc @@ -411,3 +411,10 @@ Overriding the HTTP method occurs in a filter. That filter must be placed before Spring Security's support. Note that overriding only happens on a `post`, so this is actually unlikely to cause any real problems. However, it is still best practice to ensure it is placed before Spring Security's filters. + + +[[csrf-considerations-breach-mitigation]] +==== BREACH Security Exploit Mitigation +In order to protect our application from BREACH attacks https://en.wikipedia.org/wiki/BREACH consider to use `XorCsrfToken` implementation instead of `DefaultCsrfToken`. +`XorCsrfToken` implementation provides protection from BREACH by always returning random string for the token value. +this will make it harder to guess the actual length of each response payload when using HTTP compression. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc index a7bcbc3a529..03d255b8780 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc @@ -414,3 +414,62 @@ We have <> the trade-offs In Spring's Servlet support, overriding the HTTP method is done using https://docs.spring.io/spring-framework/docs/5.2.x/javadoc-api/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.html[HiddenHttpMethodFilter]. More information can be found in https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web.html#mvc-rest-method-conversion[HTTP Method Conversion] section of the reference documentation. + +[[servlet-csrf-considerations-breach-mitigation]] +=== BREACH Security Exploit Mitigation +We have <> usage on `XorCsrfToken` implementation. + +In Spring's Servlet support, https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CookieCsrfTokenRepository.html[CookieCsrfTokenRepository] and https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.html[HttpSessionCsrfTokenRepository] use https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/DefaultCsrfToken.html[DefaultCsrfToken] instance type as return type for method `generateToken(HttpServletRequest request)` and `loadToken(HttpServletRequest request)`. + +In order to override the default implementation in both classes `setGenerateToken(GenerateTokenProvider generateTokenProvider)` is introduced. +We could set the https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/GenerateTokenProvider.html[GenerateTokenProvider] from provided static method https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/XorCsrfToken.html[XorCsrfToken.createGenerateTokenProvider()] as per below: + +.setGenerateToken from provided static method +==== +[source,java] +---- +CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository(); +cookieCsrfTokenRepository.setGenerateToken(XorCsrfToken.createGenerateTokenProvider()) +---- +==== + +or we could set by defining our own implementation + +.setGenerateToken own implementation +==== +[source,java] +---- +CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository(); +cookieCsrfTokenRepository.setGenerateToken((headerName, parameterName, value) -> new XorCsrfToken(headerName, parameterName, value)) +---- +==== + +Note that `GenerateTokenProvider` is a `FunctionalInterface` that accepts three parameters `(headerName, parameterName, value)`, +`headerName` and `parameterName` values are derived/assigned from `CookieCsrfTokenRepository` / `HttpSessionCsrfTokenRepository` fields, +`value` is a string token value generated/loaded from `CookieCsrfTokenRepository` / `HttpSessionCsrfTokenRepository`. + +We could customize `headerName` and `parameterName` in a couple of ways: + +.setGenerateToken hard coded +==== +[source,java] +---- +CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository(); +cookieCsrfTokenRepository.setGenerateToken((headerName, parameterName, value) -> new XorCsrfToken("customHeader", "customParameter", value)); +---- +==== + +.setGenerateToken from our own configuration instance +==== +[source,java] +---- +ExampleConfig exampleConfig = new ExampleConfig(); + +CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository(); +cookieCsrfTokenRepository.setGenerateToken((headerName, parameterName, value) -> new XorCsrfToken(exampleConfig.getHeaderName(), exampleConfig.getParameterName(), value)) +---- +==== + +This means in the future `headerName` and `parameterName` will be maintained outside of `CookieCsrfTokenRepository` / `HttpSessionCsrfTokenRepository` classes, +so `setHeaderName(String headerName)` and `setParameterName(String parameterName)` are deprecated and might be removed in the future version. + diff --git a/messaging/src/main/java/org/springframework/security/messaging/web/csrf/CsrfChannelInterceptor.java b/messaging/src/main/java/org/springframework/security/messaging/web/csrf/CsrfChannelInterceptor.java index c59858e1a38..7a903ad3a57 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/web/csrf/CsrfChannelInterceptor.java +++ b/messaging/src/main/java/org/springframework/security/messaging/web/csrf/CsrfChannelInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,7 @@ public Message preSend(Message message, MessageChannel channel) { String actualTokenValue = SimpMessageHeaderAccessor.wrap(message) .getFirstNativeHeader(expectedToken.getHeaderName()); - boolean csrfCheckPassed = expectedToken.getToken().equals(actualTokenValue); + boolean csrfCheckPassed = expectedToken.matches(actualTokenValue); if (csrfCheckPassed) { return message; } diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuildersFormLoginTests.java b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuildersFormLoginTests.java index 0a2501449a0..b0ed96cbb60 100644 --- a/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuildersFormLoginTests.java +++ b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuildersFormLoginTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,8 +44,7 @@ public void defaults() { assertThat(request.getParameter("username")).isEqualTo("user"); assertThat(request.getParameter("password")).isEqualTo("password"); assertThat(request.getMethod()).isEqualTo("POST"); - assertThat(request.getParameter(token.getParameterName())) - .isEqualTo(token.getToken()); + assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue(); assertThat(request.getRequestURI()).isEqualTo("/login"); assertThat(request.getParameter("_csrf")).isNotNull(); } @@ -61,8 +60,7 @@ public void custom() { assertThat(request.getParameter("username")).isEqualTo("admin"); assertThat(request.getParameter("password")).isEqualTo("secret"); assertThat(request.getMethod()).isEqualTo("POST"); - assertThat(request.getParameter(token.getParameterName())) - .isEqualTo(token.getToken()); + assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue(); assertThat(request.getRequestURI()).isEqualTo("/login"); } @@ -77,8 +75,7 @@ public void customWithUriVars() { assertThat(request.getParameter("username")).isEqualTo("admin"); assertThat(request.getParameter("password")).isEqualTo("secret"); assertThat(request.getMethod()).isEqualTo("POST"); - assertThat(request.getParameter(token.getParameterName())) - .isEqualTo(token.getToken()); + assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue(); assertThat(request.getRequestURI()).isEqualTo("/uri-login/val1/val2"); } diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuildersFormLogoutTests.java b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuildersFormLogoutTests.java index 1e86868d3e2..71822f60381 100644 --- a/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuildersFormLogoutTests.java +++ b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuildersFormLogoutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,8 +40,7 @@ public void defaults() { CsrfToken token = (CsrfToken) request.getAttribute(CsrfRequestPostProcessor.TestCsrfTokenRepository.TOKEN_ATTR_NAME); assertThat(request.getMethod()).isEqualTo("POST"); - assertThat(request.getParameter(token.getParameterName())).isEqualTo( - token.getToken()); + assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue(); assertThat(request.getRequestURI()).isEqualTo("/logout"); } @@ -53,8 +52,7 @@ public void custom() { CsrfToken token = (CsrfToken) request.getAttribute(CsrfRequestPostProcessor.TestCsrfTokenRepository.TOKEN_ATTR_NAME); assertThat(request.getMethod()).isEqualTo("POST"); - assertThat(request.getParameter(token.getParameterName())).isEqualTo( - token.getToken()); + assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue(); assertThat(request.getRequestURI()).isEqualTo("/admin/logout"); } @@ -66,8 +64,7 @@ public void customWithUriVars() { CsrfToken token = (CsrfToken) request.getAttribute(CsrfRequestPostProcessor.TestCsrfTokenRepository.TOKEN_ATTR_NAME); assertThat(request.getMethod()).isEqualTo("POST"); - assertThat(request.getParameter(token.getParameterName())).isEqualTo( - token.getToken()); + assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue(); assertThat(request.getRequestURI()).isEqualTo("/uri-logout/val1/val2"); } diff --git a/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java index 1c77c9bbad8..d7e9f411587 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java +++ b/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package org.springframework.security.web.csrf; import java.util.UUID; - +import java.util.function.Function; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -53,13 +53,15 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { private String cookieDomain; + private Function generateTokenProvider = (value) -> new DefaultCsrfToken(this.headerName, + this.parameterName, value); + public CookieCsrfTokenRepository() { } @Override public CsrfToken generateToken(HttpServletRequest request) { - return new DefaultCsrfToken(this.headerName, this.parameterName, - createNewToken()); + return this.generateTokenProvider.apply(createNewToken()); } @Override @@ -97,7 +99,7 @@ public CsrfToken loadToken(HttpServletRequest request) { if (!StringUtils.hasLength(token)) { return null; } - return new DefaultCsrfToken(this.headerName, this.parameterName, token); + return this.generateTokenProvider.apply(token); } /** @@ -105,7 +107,9 @@ public CsrfToken loadToken(HttpServletRequest request) { * * @param parameterName the name of the HTTP request parameter that should be used to * provide a token + * @deprecated use {@link #setGenerateToken(generateTokenProvider)} and pass the parameterName instead. */ + @Deprecated public void setParameterName(String parameterName) { Assert.notNull(parameterName, "parameterName is not null"); this.parameterName = parameterName; @@ -116,7 +120,9 @@ public void setParameterName(String parameterName) { * * @param headerName the name of the HTTP header that should be used to provide the * token + * @deprecated use {@link #setGenerateToken(generateTokenProvider)} and pass the headerName instead. */ + @Deprecated public void setHeaderName(String headerName) { Assert.notNull(headerName, "headerName is not null"); this.headerName = headerName; @@ -195,4 +201,21 @@ public void setCookieDomain(String cookieDomain) { this.cookieDomain = cookieDomain; } + /** + * Sets generate token provider
+ *
+ * Example :
+ *
+ * {@code (headerName, parameterName, value) -> new DefaultCsrfToken(headerName, parameterName, value)}
+ * + * @param generateTokenProvider provider to be used for generateToken and + * loadToken + * @since 5.4 + * @see GenerateTokenProvider + * @see XorCsrfToken + */ + public void setGenerateToken(GenerateTokenProvider generateTokenProvider) { + this.generateTokenProvider = (value) -> generateTokenProvider.generateToken(this.headerName, this.parameterName, + value); + } } diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java index 9a489c6f967..3ac7d50630e 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -122,7 +122,7 @@ protected void doFilterInternal(HttpServletRequest request, if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } - if (!csrfToken.getToken().equals(actualToken)) { + if (!csrfToken.matches(actualToken)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)); diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java index 7f00197cf57..4a520d0ed86 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ * Provides the information about an expected CSRF token. * * @see DefaultCsrfToken + * @see XorCsrfToken * * @author Rob Winch * @since 3.2 @@ -49,4 +50,14 @@ public interface CsrfToken extends Serializable { */ String getToken(); + /** + * Compare if this token matches with another token. + * + * @param token to be matched + * @return true if this instance token matches the token, otherwise false. + * @since 5.4 + */ + default boolean matches(String token) { + return getToken().equals(token); + } } \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/csrf/GenerateTokenProvider.java b/web/src/main/java/org/springframework/security/web/csrf/GenerateTokenProvider.java new file mode 100644 index 00000000000..8c35b3b8369 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/GenerateTokenProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.web.csrf; + +/** + * Functional interface to provide CSRF token generation logic + * + * @author Ruby Hartono + * + * @param the type of the returned CsrfToken + * @since 5.4 + */ +@FunctionalInterface +public interface GenerateTokenProvider { + + /** + * Generate CsrfToken from parameters + * + * @param headerName header name + * @param parameterName parameter name + * @param value token value + * @return CsrfToken generated from parameters + */ + T generateToken(String headerName, String parameterName, String value); +} diff --git a/web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java index a70220d24aa..ed4cb109c22 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java +++ b/web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.web.csrf; import java.util.UUID; +import java.util.function.Function; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -44,6 +45,9 @@ public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME; + private Function generateTokenProvider = (value) -> new DefaultCsrfToken(this.headerName, + this.parameterName, value); + /* * (non-Javadoc) * @@ -87,15 +91,16 @@ public CsrfToken loadToken(HttpServletRequest request) { * servlet .http.HttpServletRequest) */ public CsrfToken generateToken(HttpServletRequest request) { - return new DefaultCsrfToken(this.headerName, this.parameterName, - createNewToken()); + return generateTokenProvider.apply(createNewToken()); } /** * Sets the {@link HttpServletRequest} parameter name that the {@link CsrfToken} is * expected to appear on * @param parameterName the new parameter name to use + * @deprecated use {@link #setGenerateToken(generateTokenProvider)} and pass the parameterName instead. */ + @Deprecated public void setParameterName(String parameterName) { Assert.hasLength(parameterName, "parameterName cannot be null or empty"); this.parameterName = parameterName; @@ -106,7 +111,9 @@ public void setParameterName(String parameterName) { * header that the response will contain the {@link CsrfToken}. * * @param headerName the new header name to use + * @deprecated use {@link #setGenerateToken(generateTokenProvider)} and pass the headerName instead. */ + @Deprecated public void setHeaderName(String headerName) { Assert.hasLength(headerName, "headerName cannot be null or empty"); this.headerName = headerName; @@ -125,4 +132,22 @@ public void setSessionAttributeName(String sessionAttributeName) { private String createNewToken() { return UUID.randomUUID().toString(); } + + /** + * Sets generate token provider
+ *
+ * Example :
+ *
+ * {@code (headerName, parameterName, value) -> new DefaultCsrfToken(headerName, parameterName, value)}
+ * + * @param generateTokenProvider provider to be used for generateToken and + * loadToken + * @since 5.4 + * @see GenerateTokenProvider + * @see XorCsrfToken + */ + public void setGenerateToken(GenerateTokenProvider generateTokenProvider) { + this.generateTokenProvider = (value) -> generateTokenProvider.generateToken(this.headerName, this.parameterName, + value); + } } diff --git a/web/src/main/java/org/springframework/security/web/csrf/LazyCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/csrf/LazyCsrfTokenRepository.java index 1ce4326ad07..6806b657fc9 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/LazyCsrfTokenRepository.java +++ b/web/src/main/java/org/springframework/security/web/csrf/LazyCsrfTokenRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -128,6 +128,12 @@ public String getToken() { return this.delegate.getToken(); } + @Override + public boolean matches(String token) { + saveTokenIfNecessary(); + return this.delegate.matches(token); + } + @Override public String toString() { return "SaveOnAccessCsrfToken [delegate=" + this.delegate + "]"; diff --git a/web/src/main/java/org/springframework/security/web/csrf/XorCsrfToken.java b/web/src/main/java/org/springframework/security/web/csrf/XorCsrfToken.java new file mode 100644 index 00000000000..263ec8b4c5c --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/XorCsrfToken.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.web.csrf; + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; +import org.springframework.security.crypto.codec.Utf8; +import org.springframework.util.Assert; + +/** + * A CSRF token that is used to protect against CSRF attacks.
+ *
+ * This token provide protection from BREACH exploit by always returning a Base64Url encoded + * random string (XOR-ed token value with salt) {@link #getToken()}. In order to check if an + * instance token matches with the string value use + * {@link #matches(String)} + * + * @author Ruby Hartono + * @since 5.4 + */ +@SuppressWarnings("serial") +public final class XorCsrfToken implements CsrfToken { + + /** + * Convenient method to provide generate token + * + * @return GenerateTokenProvider that generate XorCsrfToken with + * {@link java.security.SecureRandom} empty constructor + * @see CookieCsrfTokenRepository + * @see HttpSessionCsrfTokenRepository + */ + public static GenerateTokenProvider createGenerateTokenProvider() { + return (headerName, parameterName, value) -> new XorCsrfToken(headerName, parameterName, value); + } + + /** + * Convenient method to provide generate token + * + * @param secureRandom instance to be set for the XorCsrfToken + * @return GenerateTokenProvider that that generate XorCsrfToken with + * {@link java.security.SecureRandom} from parameter + * @see CookieCsrfTokenRepository + * @see HttpSessionCsrfTokenRepository + */ + public static GenerateTokenProvider createGenerateTokenProvider(SecureRandom secureRandom) { + return (headerName, parameterName, value) -> new XorCsrfToken(headerName, parameterName, value, secureRandom); + } + + private final byte[] tokenBytes; + + private final String parameterName; + + private final String headerName; + + private final SecureRandom secureRandom; + + /** + * Creates a new instance + * + * @param headerName the HTTP header name to use + * @param parameterName the HTTP parameter name to use + * @param token the value of the token (i.e. expected value of the HTTP + * parameter of parametername). + */ + public XorCsrfToken(String headerName, String parameterName, String token) { + this(headerName, parameterName, token, new SecureRandom()); + } + + /** + * Creates a new instance + * + * @param headerName the HTTP header name to use + * @param parameterName the HTTP parameter name to use + * @param token the value of the token (i.e. expected value of the HTTP + * parameter of parametername). + * @param secureRandom secure random instance to be used for random salt + */ + public XorCsrfToken(String headerName, String parameterName, String token, SecureRandom secureRandom) { + Assert.hasLength(headerName, "headerName cannot be null or empty"); + Assert.hasLength(parameterName, "parameterName cannot be null or empty"); + Assert.hasLength(token, "token cannot be null or empty"); + this.headerName = headerName; + this.parameterName = parameterName; + this.tokenBytes = Utf8.encode(token); + this.secureRandom = secureRandom; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.security.web.csrf.CsrfToken#getHeaderName() + */ + public String getHeaderName() { + return this.headerName; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.security.web.csrf.CsrfToken#getParameterName() + */ + public String getParameterName() { + return this.parameterName; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.security.web.csrf.CsrfToken#getToken() + */ + public String getToken() { + byte[] randomBytes = new byte[this.tokenBytes.length]; + this.secureRandom.nextBytes(randomBytes); + + byte[] xoredCsrf = xorCsrf(randomBytes, this.tokenBytes); + + byte[] combinedBytes = new byte[randomBytes.length + xoredCsrf.length]; + System.arraycopy(randomBytes, 0, combinedBytes, 0, randomBytes.length); + System.arraycopy(xoredCsrf, 0, combinedBytes, randomBytes.length, xoredCsrf.length); + + // returning randomBytes + XOR csrf token + return Base64.getUrlEncoder().encodeToString(combinedBytes); + } + + public String getTokenValue() { + return Utf8.decode(this.tokenBytes); + } + + + private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { + byte[] xoredCsrf = new byte[csrfBytes.length]; + System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); + for (byte b : randomBytes) { + for (int i = 0; i < xoredCsrf.length; i++) { + xoredCsrf[i] ^= b; + } + } + + return xoredCsrf; + } + + @Override + public boolean matches(String token) { + byte[] paramToken = null; + + try { + paramToken = Base64.getUrlDecoder().decode(token); + } catch (Exception ex) { + return false; + } + + int tokenSize = this.tokenBytes.length; + + if (paramToken.length == tokenSize) { + return MessageDigest.isEqual(this.tokenBytes, paramToken); + } else if (paramToken.length < tokenSize) { + return false; + } + + // extract token and random bytes + int paramXorTokenOffset = paramToken.length - tokenSize; + byte[] paramXoredToken = new byte[tokenSize]; + byte[] paramRandomBytes = new byte[paramXorTokenOffset]; + + System.arraycopy(paramToken, 0, paramRandomBytes, 0, paramXorTokenOffset); + System.arraycopy(paramToken, paramXorTokenOffset, paramXoredToken, 0, paramXoredToken.length); + + byte[] paramActualCsrfToken = xorCsrf(paramRandomBytes, paramXoredToken); + + // comparing this token with the actual csrf token from param + return MessageDigest.isEqual(this.tokenBytes, paramActualCsrfToken); + } +} diff --git a/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java b/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java index 7ec2fdb3890..1743e5cbada 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,6 +69,61 @@ public void generateTokenCustom() { assertThat(generateToken.getToken()).isNotEmpty(); } + @Test + public void customGenerateToken() { + this.repository.setGenerateToken(XorCsrfToken.createGenerateTokenProvider()); + CsrfToken generateToken = this.repository.generateToken(this.request); + + assertThat(generateToken).isNotNull(); + assertThat(generateToken).isInstanceOf(XorCsrfToken.class); + assertThat(generateToken.getHeaderName()) + .isEqualTo(CookieCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME); + assertThat(generateToken.getParameterName()) + .isEqualTo(CookieCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME); + assertThat(generateToken.getToken()).isNotEmpty(); + } + + @Test + public void customGenerateTokenWithCustomHeaderAndParameter() { + // hardcoded the headerName and parameterName instead of using this.repository.setHeaderName + this.repository.setGenerateToken( + (pHeaderName, pParameterName, tokenValue) -> new DefaultCsrfToken("header", "parameter", tokenValue)); + + CsrfToken generateToken = this.repository.generateToken(this.request); + + assertThat(generateToken).isNotNull(); + assertThat(generateToken.getHeaderName()).isEqualTo("header"); + assertThat(generateToken.getParameterName()).isEqualTo("parameter"); + assertThat(generateToken.getToken()).isNotEmpty(); + } + + @Test + public void customGenerateTokenWithCustomHeaderAndParameterFromInstance() { + // a sample test where configuration instance was used to maintain headerName and parameterName + class ParameterConfiguration { + String header = "header"; + String parameter = "parameter"; + } + + ParameterConfiguration paramConfig = new ParameterConfiguration(); + + // set the header and parameter + this.repository.setGenerateToken((pHeaderName, pParameterName, + tokenValue) -> new DefaultCsrfToken(paramConfig.header, paramConfig.parameter, tokenValue)); + + // if instance was modified then it will reflect on the generated token + paramConfig.header = "customHeader"; + paramConfig.parameter = "customParameter"; + + CsrfToken generateToken = this.repository.generateToken(this.request); + + assertThat(generateToken).isNotNull(); + assertThat(generateToken).isInstanceOf(DefaultCsrfToken.class); + assertThat(generateToken.getHeaderName()).isEqualTo("customHeader"); + assertThat(generateToken.getParameterName()).isEqualTo("customParameter"); + assertThat(generateToken.getToken()).isNotEmpty(); + } + @Test public void saveToken() { CsrfToken token = this.repository.generateToken(this.request); @@ -82,7 +137,7 @@ public void saveToken() { .isEqualTo(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath()); assertThat(tokenCookie.getSecure()).isEqualTo(this.request.isSecure()); - assertThat(tokenCookie.getValue()).isEqualTo(token.getToken()); + assertThat(token.matches(tokenCookie.getValue())).isTrue(); assertThat(tokenCookie.isHttpOnly()).isEqualTo(true); } @@ -257,7 +312,26 @@ public void loadTokenCustom() { assertThat(loadToken).isNotNull(); assertThat(loadToken.getHeaderName()).isEqualTo(headerName); assertThat(loadToken.getParameterName()).isEqualTo(parameterName); - assertThat(loadToken.getToken()).isEqualTo(value); + assertThat(loadToken.matches(value)).isTrue(); + } + + @Test + public void loadTokenWithCustomGenerateToken() { + this.repository.setGenerateToken(XorCsrfToken.createGenerateTokenProvider()); + CsrfToken generateToken = this.repository.generateToken(this.request); + + this.request + .setCookies(new Cookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, + generateToken.getToken())); + + CsrfToken loadToken = this.repository.loadToken(this.request); + + assertThat(loadToken).isNotNull(); + assertThat(loadToken).isInstanceOf(XorCsrfToken.class); + assertThat(loadToken.getHeaderName()).isEqualTo(generateToken.getHeaderName()); + assertThat(loadToken.getParameterName()) + .isEqualTo(generateToken.getParameterName()); + assertThat(loadToken.getToken()).isNotEmpty(); } @Test(expected = IllegalArgumentException.class) diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java index 0d888a4133a..d0a60acf90a 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java +++ b/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,7 +85,7 @@ public void logoutRemovesCsrfTokenAndSavesNew() { // SEC-2404, SEC-2832 CsrfToken tokenInRequest = (CsrfToken) this.request .getAttribute(CsrfToken.class.getName()); - assertThat(tokenInRequest.getToken()).isSameAs(this.generatedToken.getToken()); + assertThat(tokenInRequest.matches(this.generatedToken.getToken())).isTrue(); assertThat(tokenInRequest.getHeaderName()) .isSameAs(this.generatedToken.getHeaderName()); assertThat(tokenInRequest.getParameterName()) diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java index 4129201cb42..b675f25986c 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -439,7 +439,7 @@ public CsrfTokenAssert isEqualTo(CsrfToken expected) { assertThat(this.actual.getHeaderName()).isEqualTo(expected.getHeaderName()); assertThat(this.actual.getParameterName()) .isEqualTo(expected.getParameterName()); - assertThat(this.actual.getToken()).isEqualTo(expected.getToken()); + assertThat(this.actual.matches(expected.getToken())).isTrue(); return this; } } diff --git a/web/src/test/java/org/springframework/security/web/csrf/DefaultCsrfTokenTests.java b/web/src/test/java/org/springframework/security/web/csrf/DefaultCsrfTokenTests.java index 2e9ff025f4b..9fb69017cdd 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/DefaultCsrfTokenTests.java +++ b/web/src/test/java/org/springframework/security/web/csrf/DefaultCsrfTokenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ package org.springframework.security.web.csrf; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.Test; /** @@ -55,4 +57,30 @@ public void constructorNullTokenValue() { public void constructorEmptyTokenValue() { new DefaultCsrfToken(headerName, parameterName, ""); } + + @Test + public void matchesTokenValue() { + String tokenStr = "123456"; + DefaultCsrfToken token = new DefaultCsrfToken(headerName, parameterName, tokenStr); + String csrfToken = token.getToken(); + String csrfToken2 = token.getToken(); + + assertThat(token.getToken()).isEqualTo(csrfToken); + assertThat(token.getToken()).isEqualTo(csrfToken2); + assertThat(csrfToken).isEqualTo(csrfToken2); + assertThat(token.matches(csrfToken)).isTrue(); + assertThat(token.matches(csrfToken2)).isTrue(); + } + + @Test + public void notMatchesTokenValue() { + DefaultCsrfToken token1 = new DefaultCsrfToken(headerName, parameterName, "token1"); + DefaultCsrfToken token2 = new DefaultCsrfToken(headerName, parameterName, "token2"); + String csrfToken1 = token1.getToken(); + String csrfToken2 = token2.getToken(); + + assertThat(csrfToken1).isNotEqualTo(csrfToken2); + assertThat(token1.matches(csrfToken2)).isFalse(); + assertThat(token2.matches(csrfToken1)).isFalse(); + } } diff --git a/web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java b/web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java index 87934b474af..c1728249b66 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,6 +53,61 @@ public void generateToken() { assertThat(loadedToken).isNull(); } + @Test + public void customGenerateToken() { + repo.setGenerateToken(XorCsrfToken.createGenerateTokenProvider()); + token = repo.generateToken(request); + + assertThat(token).isInstanceOf(XorCsrfToken.class); + assertThat(token.getParameterName()).isEqualTo("_csrf"); + assertThat(token.getToken()).isNotEmpty(); + + CsrfToken loadedToken = repo.loadToken(request); + + assertThat(loadedToken).isNull(); + } + + @Test + public void customGenerateTokenWithCustomHeaderAndParameter() { + // hardcoded the headerName and parameterName instead of using this.repository.setHeaderName + this.repo.setGenerateToken( + (pHeaderName, pParameterName, tokenValue) -> new DefaultCsrfToken("header", "parameter", tokenValue)); + + CsrfToken generateToken = this.repo.generateToken(this.request); + + assertThat(generateToken).isNotNull(); + assertThat(generateToken.getHeaderName()).isEqualTo("header"); + assertThat(generateToken.getParameterName()).isEqualTo("parameter"); + assertThat(generateToken.getToken()).isNotEmpty(); + } + + @Test + public void customGenerateTokenWithCustomHeaderAndParameterFromInstance() { + // a sample test where configuration instance was used to maintain headerName and parameterName + class ParameterConfiguration { + String header = "header"; + String parameter = "parameter"; + } + + ParameterConfiguration paramInstance = new ParameterConfiguration(); + + // set the header and parameter + this.repo.setGenerateToken((pHeaderName, pParameterName, + tokenValue) -> new DefaultCsrfToken(paramInstance.header, paramInstance.parameter, tokenValue)); + + // if instance was modified then it will reflect on the generated token + paramInstance.header = "customHeader"; + paramInstance.parameter = "customParameter"; + + CsrfToken generateToken = this.repo.generateToken(this.request); + + assertThat(generateToken).isNotNull(); + assertThat(generateToken).isInstanceOf(DefaultCsrfToken.class); + assertThat(generateToken.getHeaderName()).isEqualTo("customHeader"); + assertThat(generateToken.getParameterName()).isEqualTo("customParameter"); + assertThat(generateToken.getToken()).isNotEmpty(); + } + @Test public void generateCustomParameter() { String paramName = "_csrf"; @@ -98,6 +153,20 @@ public void saveToken() { assertThat(loadedToken).isEqualTo(tokenToSave); } + @Test + public void saveTokenWithCustomGenerateToken() { + repo.setGenerateToken(XorCsrfToken.createGenerateTokenProvider()); + CsrfToken tokenToSave = repo.generateToken(request); + repo.saveToken(tokenToSave, request, response); + + CsrfToken loadedToken = (CsrfToken) repo.loadToken(request); + + assertThat(tokenToSave).isInstanceOf(XorCsrfToken.class); + assertThat(loadedToken).isInstanceOf(XorCsrfToken.class); + assertThat(loadedToken).isSameAs(tokenToSave); + assertThat(loadedToken.matches(tokenToSave.getToken())).isTrue(); + } + @Test public void saveTokenCustomSessionAttribute() { CsrfToken tokenToSave = new DefaultCsrfToken("123", "abc", "def"); diff --git a/web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenTests.java b/web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenTests.java new file mode 100644 index 00000000000..7d9bffe6d68 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.web.csrf; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; + +/** + * @author Ruby Hartono + * + */ +public class XorCsrfTokenTests { + private final String headerName = "headerName"; + private final String parameterName = "parameterName"; + private final String tokenValue = "tokenValue"; + + @Test(expected = IllegalArgumentException.class) + public void constructorNullHeaderName() { + new XorCsrfToken(null, parameterName, tokenValue); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorEmptyHeaderName() { + new XorCsrfToken("", parameterName, tokenValue); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullParameterName() { + new XorCsrfToken(headerName, null, tokenValue); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorEmptyParameterName() { + new XorCsrfToken(headerName, "", tokenValue); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullTokenValue() { + new XorCsrfToken(headerName, parameterName, null); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorEmptyTokenValue() { + new XorCsrfToken(headerName, parameterName, ""); + } + + @Test + public void matchesTokenValue() { + String tokenStr = "123456"; + XorCsrfToken token = new XorCsrfToken(headerName, parameterName, tokenStr); + String randomCsrfToken = token.getToken(); + String randomCsrfToken2 = token.getToken(); + + assertThat(token.getToken()).isNotEqualTo(randomCsrfToken); + assertThat(token.getToken()).isNotEqualTo(randomCsrfToken2); + assertThat(randomCsrfToken).isNotEqualTo(randomCsrfToken2); + assertThat(token.matches(randomCsrfToken)).isTrue(); + assertThat(token.matches(randomCsrfToken2)).isTrue(); + } + + @Test + public void notMatchesTokenValue() { + XorCsrfToken token1 = new XorCsrfToken(headerName, parameterName, "token1"); + XorCsrfToken token2 = new XorCsrfToken(headerName, parameterName, "token2"); + String randomCsrfToken1 = token1.getToken(); + String randomCsrfToken2 = token2.getToken(); + + assertThat(randomCsrfToken1).isNotEqualTo(randomCsrfToken2); + assertThat(token1.matches(randomCsrfToken2)).isFalse(); + assertThat(token2.matches(randomCsrfToken1)).isFalse(); + } + + @Test + public void createGenerateTokenProviderShouldReturnInstanceWithSameBehaviorAsConstructorCreation() { + XorCsrfToken token1 = new XorCsrfToken(headerName, parameterName, "token1"); + XorCsrfToken tokenFromProvider = XorCsrfToken.createGenerateTokenProvider().generateToken(headerName, + parameterName, "token1"); + String randomCsrfToken1 = token1.getToken(); + String randomCsrfToken2 = tokenFromProvider.getToken(); + + assertThat(randomCsrfToken1).isNotEqualTo(randomCsrfToken2); + assertThat(token1.matches(randomCsrfToken1)).isTrue(); + assertThat(token1.matches(randomCsrfToken2)).isTrue(); + assertThat(tokenFromProvider.matches(randomCsrfToken1)).isTrue(); + assertThat(tokenFromProvider.matches(randomCsrfToken2)).isTrue(); + } +} diff --git a/web/src/test/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixinTests.java index 58f15889ed6..bb2a0d68aff 100644 --- a/web/src/test/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixinTests.java +++ b/web/src/test/java/org/springframework/security/web/jackson2/DefaultCsrfTokenMixinTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import org.json.JSONException; import org.junit.Test; import org.skyscreamer.jsonassert.JSONAssert; - import org.springframework.security.web.csrf.DefaultCsrfToken; import static org.assertj.core.api.Assertions.assertThat; @@ -56,7 +55,7 @@ public void defaultCsrfTokenDeserializeTest() throws IOException { assertThat(token).isNotNull(); assertThat(token.getHeaderName()).isEqualTo("csrf-header"); assertThat(token.getParameterName()).isEqualTo("_csrf"); - assertThat(token.getToken()).isEqualTo("1"); + assertThat(token.matches("1")).isTrue(); } @Test(expected = JsonMappingException.class) diff --git a/web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java b/web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java index eac675d5572..3d168cb93f1 100644 --- a/web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java +++ b/web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - import org.junit.Before; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -39,7 +36,6 @@ public class CsrfRequestDataValueProcessorTests { private CsrfRequestDataValueProcessor processor; private CsrfToken token; - private Map expected = new HashMap<>(); @Before public void setup() { @@ -48,8 +44,6 @@ public void setup() { token = new DefaultCsrfToken("1", "a", "b"); request.setAttribute(CsrfToken.class.getName(), token); - - expected.put(token.getParameterName(), token.getToken()); } @Test @@ -73,7 +67,7 @@ public void getExtraHiddenFieldsNoCsrfToken() { @Test public void getExtraHiddenFieldsHasCsrfTokenNoMethodSet() { - assertThat(processor.getExtraHiddenFields(request)).isEqualTo(expected); + assertThat(token.matches(processor.getExtraHiddenFields(request).getOrDefault(token.getParameterName(), null))).isTrue(); } @Test @@ -91,13 +85,13 @@ public void getExtraHiddenFieldsHasCsrfToken_get() { @Test public void getExtraHiddenFieldsHasCsrfToken_POST() { processor.processAction(request, "action", "POST"); - assertThat(processor.getExtraHiddenFields(request)).isEqualTo(expected); + assertThat(token.matches(processor.getExtraHiddenFields(request).getOrDefault(token.getParameterName(), null))).isTrue(); } @Test public void getExtraHiddenFieldsHasCsrfToken_post() { processor.processAction(request, "action", "post"); - assertThat(processor.getExtraHiddenFields(request)).isEqualTo(expected); + assertThat(token.matches(processor.getExtraHiddenFields(request).getOrDefault(token.getParameterName(), null))).isTrue(); } @Test @@ -129,10 +123,8 @@ public void processUrl() { public void createGetExtraHiddenFieldsHasCsrfToken() { CsrfToken token = new DefaultCsrfToken("1", "a", "b"); request.setAttribute(CsrfToken.class.getName(), token); - Map expected = new HashMap<>(); - expected.put(token.getParameterName(), token.getToken()); RequestDataValueProcessor processor = new CsrfRequestDataValueProcessor(); - assertThat(processor.getExtraHiddenFields(request)).isEqualTo(expected); + assertThat(token.matches(processor.getExtraHiddenFields(request).getOrDefault(token.getParameterName(), null))).isTrue(); } }