From e239b035b91b4af6e0cc2092e6779c90df83d499 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 1/2] 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` Closes gh-4001 Co-Authored-By: Rob Winch --- .../security/config/http/CsrfConfigTests.java | 3 +- ...yMockMvcRequestBuildersFormLoginTests.java | 5 +- ...MockMvcRequestBuildersFormLogoutTests.java | 5 +- .../web/csrf/CookieCsrfTokenRepository.java | 31 ++- .../security/web/csrf/CsrfFilter.java | 1 + .../security/web/csrf/CsrfToken.java | 13 +- .../web/csrf/GenerateTokenProvider.java | 38 ++++ .../csrf/HttpSessionCsrfTokenRepository.java | 30 ++- .../web/csrf/LazyCsrfTokenRepository.java | 8 +- .../security/web/csrf/XorCsrfToken.java | 188 ++++++++++++++++++ .../csrf/CookieCsrfTokenRepositoryTests.java | 80 +++++++- .../security/web/csrf/CsrfFilterTests.java | 2 +- .../web/csrf/DefaultCsrfTokenTests.java | 29 ++- .../HttpSessionCsrfTokenRepositoryTests.java | 71 ++++++- .../security/web/csrf/XorCsrfTokenTests.java | 100 ++++++++++ .../jackson2/DefaultCsrfTokenMixinTests.java | 3 +- .../CsrfRequestDataValueProcessorTests.java | 6 +- 17 files changed, 595 insertions(+), 18 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 99a4465da37..cc88afe534e 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-2022 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. @@ -555,6 +555,7 @@ public void match(MvcResult result) throws Exception { 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/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 9dea5175bfd..db77f1752fe 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-2022 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,6 +58,7 @@ public void defaults() { 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(); } @@ -72,6 +73,7 @@ public void custom() { 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"); } @@ -85,6 +87,7 @@ public void customWithUriVars() { 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 df6e7cfef21..b11b9df6bd2 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-2022 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. @@ -56,6 +56,7 @@ public void defaults() { .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"); } @@ -66,6 +67,7 @@ public void custom() { .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"); } @@ -77,6 +79,7 @@ public void customWithUriVars() { .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 62075e0d063..629fca06ad5 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,6 +17,7 @@ package org.springframework.security.web.csrf; import java.util.UUID; +import java.util.function.Function; import jakarta.servlet.ServletRequest; import jakarta.servlet.http.Cookie; @@ -59,12 +60,15 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { private int cookieMaxAge = -1; + 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 @@ -91,14 +95,16 @@ public CsrfToken loadToken(HttpServletRequest request) { if (!StringUtils.hasLength(token)) { return null; } - return new DefaultCsrfToken(this.headerName, this.parameterName, token); + return this.generateTokenProvider.apply(token); } /** * Sets the name of the HTTP request parameter that should be used to provide a token. * @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 cannot be null"); this.parameterName = parameterName; @@ -108,7 +114,9 @@ public void setParameterName(String parameterName) { * Sets the name of the HTTP header that should be used to provide the token. * @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 cannot be null"); this.headerName = headerName; @@ -220,4 +228,21 @@ public void setCookieMaxAge(int cookieMaxAge) { this.cookieMaxAge = cookieMaxAge; } + /** + * 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 53a244d6b09..90837befe64 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 @@ -121,6 +121,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } + // if (!csrfToken.matches(actualToken)) { // TODO: Fix default matches() method to use constant time. if (!equalsConstantTime(csrfToken.getToken(), actualToken)) { this.logger.debug( LogMessage.of(() -> "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 bc59a2e496a..a260ec05e8f 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. @@ -24,6 +24,7 @@ * @author Rob Winch * @since 3.2 * @see DefaultCsrfToken + * @see XorCsrfToken */ public interface CsrfToken extends Serializable { @@ -47,4 +48,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); + } } 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 802dcc2c064..7fe4504663d 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. @@ -17,6 +17,7 @@ package org.springframework.security.web.csrf; import java.util.UUID; +import java.util.function.Function; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -46,6 +47,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); + @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (token == null) { @@ -71,14 +75,16 @@ public CsrfToken loadToken(HttpServletRequest request) { @Override 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; @@ -88,7 +94,9 @@ public void setParameterName(String parameterName) { * Sets the header name that the {@link CsrfToken} is expected to appear on and the * 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; @@ -107,4 +115,22 @@ 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 d5c0c211904..3618dd4cbca 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. @@ -126,6 +126,12 @@ public String getToken() { return this.delegate.getToken(); } + @Override + public boolean matches(String token) { + saveTokenIfNecessary(); + return this.delegate.matches(token); + } + @Override public boolean equals(Object obj) { if (this == obj) { 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 90b33b4b165..0822fc3c437 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); @@ -78,7 +133,7 @@ public void saveToken() { assertThat(tokenCookie.getName()).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); } @@ -243,7 +298,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 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 91136b14a54..f105804e08d 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 @@ -361,7 +361,7 @@ protected CsrfTokenAssert(CsrfToken actual) { 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 f17542d04c0..bc5491e1bd7 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. @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** @@ -68,4 +69,30 @@ public void constructorEmptyTokenValue() { .isThrownBy(() -> new DefaultCsrfToken(this.headerName, this.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 13d9fca65de..5928b42a33e 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. @@ -55,6 +55,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"; @@ -94,6 +149,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 5a80c9ebe7e..97636816a21 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-2022 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. @@ -57,6 +57,7 @@ public void defaultCsrfTokenDeserializeTest() throws IOException { assertThat(token.getHeaderName()).isEqualTo("csrf-header"); assertThat(token.getParameterName()).isEqualTo("_csrf"); assertThat(token.getToken()).isEqualTo("1"); + assertThat(token.matches("1")).isTrue(); } @Test 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 b01a5cf6db5..78520d218f0 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-2022 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. @@ -74,6 +74,7 @@ public void getExtraHiddenFieldsNoCsrfToken() { @Test public void getExtraHiddenFieldsHasCsrfTokenNoMethodSet() { assertThat(this.processor.getExtraHiddenFields(this.request)).isEqualTo(this.expected); + assertThat(token.matches(processor.getExtraHiddenFields(request).get(token.getParameterName()))).isTrue(); } @Test @@ -92,12 +93,14 @@ public void getExtraHiddenFieldsHasCsrfToken_get() { public void getExtraHiddenFieldsHasCsrfToken_POST() { this.processor.processAction(this.request, "action", "POST"); assertThat(this.processor.getExtraHiddenFields(this.request)).isEqualTo(this.expected); + assertThat(token.matches(processor.getExtraHiddenFields(request).get(token.getParameterName()))).isTrue(); } @Test public void getExtraHiddenFieldsHasCsrfToken_post() { this.processor.processAction(this.request, "action", "post"); assertThat(this.processor.getExtraHiddenFields(this.request)).isEqualTo(this.expected); + assertThat(token.matches(processor.getExtraHiddenFields(request).get(token.getParameterName()))).isTrue(); } @Test @@ -132,6 +135,7 @@ public void createGetExtraHiddenFieldsHasCsrfToken() { expected.put(token.getParameterName(), token.getToken()); RequestDataValueProcessor processor = new CsrfRequestDataValueProcessor(); assertThat(processor.getExtraHiddenFields(this.request)).isEqualTo(expected); + assertThat(token.matches(processor.getExtraHiddenFields(request).get(token.getParameterName()))).isTrue(); } } From 24426845285d137d85e23391ca51fba8e329c1d1 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 21 Jan 2022 16:32:16 -0600 Subject: [PATCH 2/2] Polish gh-8082 --- .../ROOT/pages/features/exploits/csrf.adoc | 16 ++ .../ROOT/pages/servlet/exploits/csrf.adoc | 57 ++++++ .../web/csrf/CookieCsrfTokenRepository.java | 94 ++++++--- .../security/web/csrf/CsrfFilter.java | 27 +-- .../security/web/csrf/CsrfToken.java | 27 ++- .../security/web/csrf/DefaultCsrfToken.java | 94 ++++++++- .../web/csrf/GenerateTokenProvider.java | 38 ---- .../csrf/HttpSessionCsrfTokenRepository.java | 107 +++++++--- .../web/csrf/LazyCsrfTokenRepository.java | 2 +- .../security/web/csrf/XorCsrfToken.java | 188 ------------------ .../csrf/CookieCsrfTokenRepositoryTests.java | 158 ++++++++------- .../security/web/csrf/CsrfFilterTests.java | 27 ++- .../web/csrf/DefaultCsrfTokenTests.java | 31 +-- .../HttpSessionCsrfTokenRepositoryTests.java | 157 ++++++++------- .../security/web/csrf/XorCsrfTokenTests.java | 100 ---------- .../CsrfRequestDataValueProcessorTests.java | 14 +- 16 files changed, 548 insertions(+), 589 deletions(-) delete mode 100644 web/src/main/java/org/springframework/security/web/csrf/GenerateTokenProvider.java delete mode 100644 web/src/main/java/org/springframework/security/web/csrf/XorCsrfToken.java delete mode 100644 web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenTests.java diff --git a/docs/modules/ROOT/pages/features/exploits/csrf.adoc b/docs/modules/ROOT/pages/features/exploits/csrf.adoc index 7f800be3e2b..7b228dfff61 100644 --- a/docs/modules/ROOT/pages/features/exploits/csrf.adoc +++ b/docs/modules/ROOT/pages/features/exploits/csrf.adoc @@ -412,3 +412,19 @@ Overriding the HTTP method occurs in a filter. That filter must be placed before Spring Security's support. Note that overriding happens only on a `post`, so this is actually unlikely to cause any real problems. However, it is still best practice to ensure that it is placed before Spring Security's filters. + +[[csrf-considerations-breach-mitigation]] +==== BREACH Security Exploit Mitigation + +When a web server has HTTP compression enabled, web applications hosted on that server may be vulnerable to a https://en.wikipedia.org/wiki/BREACH[BREACH attack]. +Any sensitive information displayed by the web application could become compromised, including a CSRF token. +As part of a defense in depth strategy, Spring Security provides BREACH mitigation support for CSRF tokens. +When enabled, a random string is always returned for the token value, which makes it more difficult to guess the csrf token when using HTTP compression. + +NOTE: Spring Security cannot protect an application from BREACH attacks entirely, and only provides this protection as part of a defense in depth strategy. +There may be other areas of an application that are vulnerable, and therefore additional mitigation may be required. + +Use of the BREACH mitigation support is an opt-in feature in Spring Security 5.7, and will only be enabled by default starting with the 6.0 release. The following `CsrfTokenRepository` implementations provide BREACH mitigation support via the `setXorRandomSecretEnabled(true)` method: + +* `HttpSessionCsrfTokenRepository` +* `CookieCsrfTokenRepository` diff --git a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc index 8379f804141..1d41f331b9e 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc @@ -482,3 +482,60 @@ We have xref:features/exploits/csrf.adoc#csrf-considerations-multipart-body[alre In Spring's Servlet support, overriding the HTTP method is done by using https://docs.spring.io/spring-framework/docs/5.2.x/javadoc-api/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.html[`HiddenHttpMethodFilter`]. You can find more information in the 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 xref:features/exploits/csrf.adoc#csrf-considerations-breach-mitigation[already discussed] BREACH mitigation support for CSRF tokens. + +In Spring’s Servlet support, we can override the default implementation as in the example below: + +.Override implementation of `CsrfTokenRepository` +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class WebSecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf + .csrfTokenRepository(HttpSessionCsrfTokenRepository.withXorRandomSecretEnabled()) + ); + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class SecurityConfig { + + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + csrf { + csrfTokenRepository = HttpSessionCsrfTokenRepository.withXorRandomSecretEnabled() + } + } + return http.build() + } +} +---- + +.XML +[source,xml,role="secondary"] +---- + + + + + +---- +==== 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 629fca06ad5..d639a57d5f1 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-2020 the original author or authors. + * Copyright 2012-2022 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,8 +16,8 @@ package org.springframework.security.web.csrf; +import java.security.SecureRandom; import java.util.UUID; -import java.util.function.Function; import jakarta.servlet.ServletRequest; import jakarta.servlet.http.Cookie; @@ -60,20 +60,26 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { private int cookieMaxAge = -1; - private Function generateTokenProvider = (value) -> new DefaultCsrfToken(this.headerName, - this.parameterName, value); + private SecureRandom secureRandom; public CookieCsrfTokenRepository() { } + private CookieCsrfTokenRepository(SecureRandom secureRandom) { + Assert.notNull(secureRandom, "secureRandom cannot be null"); + this.secureRandom = secureRandom; + } + @Override public CsrfToken generateToken(HttpServletRequest request) { - return this.generateTokenProvider.apply(createNewToken()); + return this.isXorRandomSecretEnabled() + ? new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken(), this.secureRandom) + : new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { - String tokenValue = (token != null) ? token.getToken() : ""; + String tokenValue = getTokenValue(token); Cookie cookie = new Cookie(this.cookieName, tokenValue); cookie.setSecure((this.secure != null) ? this.secure : request.isSecure()); cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request)); @@ -95,16 +101,16 @@ public CsrfToken loadToken(HttpServletRequest request) { if (!StringUtils.hasLength(token)) { return null; } - return this.generateTokenProvider.apply(token); + return this.isXorRandomSecretEnabled() + ? new DefaultCsrfToken(this.headerName, this.parameterName, token, this.secureRandom) + : new DefaultCsrfToken(this.headerName, this.parameterName, token); } /** * Sets the name of the HTTP request parameter that should be used to provide a token. * @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 cannot be null"); this.parameterName = parameterName; @@ -114,9 +120,7 @@ public void setParameterName(String parameterName) { * Sets the name of the HTTP header that should be used to provide the token. * @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 cannot be null"); this.headerName = headerName; @@ -142,6 +146,20 @@ public void setCookieHttpOnly(boolean cookieHttpOnly) { this.cookieHttpOnly = cookieHttpOnly; } + /** + * Enables generating random secrets to XOR with the csrf token on each request. + * @param enabled {@code true} sets the {@link SecureRandom} used for generating + * random secrets, {@code false} causes it to be set to null + */ + public void setXorRandomSecretEnabled(boolean enabled) { + if (enabled) { + this.secureRandom = new SecureRandom(); + } + else { + this.secureRandom = null; + } + } + private String getRequestContext(HttpServletRequest request) { String contextPath = request.getContextPath(); return (contextPath.length() > 0) ? contextPath : "/"; @@ -159,6 +177,43 @@ public static CookieCsrfTokenRepository withHttpOnlyFalse() { return result; } + /** + * Factory method to create an instance of {@link CookieCsrfTokenRepository} that has + * {@link #setXorRandomSecretEnabled(boolean)} set to {@code true}. + * @return an instance of {@link CookieCsrfTokenRepository} with + * {@link #setXorRandomSecretEnabled(boolean)} set to {@code true} + */ + public static CookieCsrfTokenRepository withXorRandomSecretEnabled() { + CookieCsrfTokenRepository result = new CookieCsrfTokenRepository(); + result.setXorRandomSecretEnabled(true); + return result; + } + + /** + * Factory method to create an instance of {@link CookieCsrfTokenRepository} that has + * {@link #setXorRandomSecretEnabled(boolean)} set to {@code true}. + * @param secureRandom the {@link SecureRandom} to use for generating random secrets + * @return an instance of {@link CookieCsrfTokenRepository} with + * {@link #setXorRandomSecretEnabled(boolean)} set to {@code true} + */ + public static CookieCsrfTokenRepository withXorRandomSecretEnabled(SecureRandom secureRandom) { + return new CookieCsrfTokenRepository(secureRandom); + } + + private boolean isXorRandomSecretEnabled() { + return (this.secureRandom != null); + } + + private String getTokenValue(CsrfToken token) { + if (token == null) { + return ""; + } + else if (token instanceof DefaultCsrfToken) { + return ((DefaultCsrfToken) token).getRawToken(); + } + return token.getToken(); + } + private String createNewToken() { return UUID.randomUUID().toString(); } @@ -228,21 +283,4 @@ public void setCookieMaxAge(int cookieMaxAge) { this.cookieMaxAge = cookieMaxAge; } - /** - * 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 90837befe64..58f6029a222 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-2021 the original author or authors. + * Copyright 2002-2022 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,6 @@ package org.springframework.security.web.csrf; import java.io.IOException; -import java.security.MessageDigest; import java.util.Arrays; import java.util.HashSet; @@ -32,7 +31,6 @@ import org.springframework.core.log.LogMessage; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.crypto.codec.Utf8; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandlerImpl; import org.springframework.security.web.util.UrlUtils; @@ -121,8 +119,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } - // if (!csrfToken.matches(actualToken)) { // TODO: Fix default matches() method to use constant time. - if (!equalsConstantTime(csrfToken.getToken(), actualToken)) { + // Default matches method uses constant-time comparison + if (!csrfToken.matches(actualToken)) { this.logger.debug( LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request))); AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken) @@ -168,25 +166,6 @@ public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { this.accessDeniedHandler = accessDeniedHandler; } - /** - * Constant time comparison to prevent against timing attacks. - * @param expected - * @param actual - * @return - */ - private static boolean equalsConstantTime(String expected, String actual) { - if (expected == actual) { - return true; - } - if (expected == null || actual == null) { - return false; - } - // Encode after ensure that the string is not null - byte[] expectedBytes = Utf8.encode(expected); - byte[] actualBytes = Utf8.encode(actual); - return MessageDigest.isEqual(expectedBytes, actualBytes); - } - private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private final HashSet allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); 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 a260ec05e8f..d7d16afb66c 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-2020 the original author or authors. + * Copyright 2002-2022 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,6 +17,9 @@ package org.springframework.security.web.csrf; import java.io.Serializable; +import java.security.MessageDigest; + +import org.springframework.security.crypto.codec.Utf8; /** * Provides the information about an expected CSRF token. @@ -24,7 +27,6 @@ * @author Rob Winch * @since 3.2 * @see DefaultCsrfToken - * @see XorCsrfToken */ public interface CsrfToken extends Serializable { @@ -49,13 +51,24 @@ public interface CsrfToken extends Serializable { String getToken(); /** - * Compare if this token matches with another token. - * - * @param token to be matched + * Determine if this token matches with another token. Implementations should use a + * constant time comparison to protect against timing attacks. + * @param token The token to be matched against * @return true if this instance token matches the token, otherwise false. - * @since 5.4 + * @since 5.7 */ default boolean matches(String token) { - return getToken().equals(token); + String expectedToken = getToken(); + if (expectedToken == token) { + return true; + } + if (expectedToken == null || token == null) { + return false; + } + // Encode after ensuring that the string is not null + byte[] expectedBytes = Utf8.encode(expectedToken); + byte[] actualBytes = Utf8.encode(token); + return MessageDigest.isEqual(expectedBytes, actualBytes); } + } diff --git a/web/src/main/java/org/springframework/security/web/csrf/DefaultCsrfToken.java b/web/src/main/java/org/springframework/security/web/csrf/DefaultCsrfToken.java index 682be4b1dd4..f2b7f0be0ab 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/DefaultCsrfToken.java +++ b/web/src/main/java/org/springframework/security/web/csrf/DefaultCsrfToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2022 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,11 @@ 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; /** @@ -33,6 +38,8 @@ public final class DefaultCsrfToken implements CsrfToken { private final String headerName; + private final transient SecureRandom secureRandom; + /** * Creates a new instance * @param headerName the HTTP header name to use @@ -47,6 +54,26 @@ public DefaultCsrfToken(String headerName, String parameterName, String token) { this.headerName = headerName; this.parameterName = parameterName; this.token = token; + this.secureRandom = null; + } + + /** + * 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 The {@link SecureRandom} to use for generating salt values + */ + DefaultCsrfToken(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"); + Assert.notNull(secureRandom, "secureRandom cannot be null"); + this.headerName = headerName; + this.parameterName = parameterName; + this.token = token; + this.secureRandom = secureRandom; } @Override @@ -61,7 +88,72 @@ public String getParameterName() { @Override public String getToken() { + return this.isXorRandomSecretEnabled() ? this.createXoredCsrfToken() : this.token; + } + + @Override + public boolean matches(String token) { + if (!this.isXorRandomSecretEnabled()) { + return CsrfToken.super.matches(token); + } + + byte[] actualBytes; + try { + actualBytes = Base64.getUrlDecoder().decode(token); + } + catch (Exception ex) { + return false; + } + + byte[] tokenBytes = Utf8.encode(this.token); + int tokenSize = tokenBytes.length; + if (actualBytes.length < tokenSize) { + return false; + } + + // extract token and random bytes + int randomBytesSize = actualBytes.length - tokenSize; + byte[] xoredCsrf = new byte[tokenSize]; + byte[] randomBytes = new byte[randomBytesSize]; + + System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize); + System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize); + + byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf); + + // comparing this token with the actual csrf token from param + return MessageDigest.isEqual(tokenBytes, csrfBytes); + } + + String getRawToken() { return this.token; } + private boolean isXorRandomSecretEnabled() { + return (this.secureRandom != null); + } + + private String createXoredCsrfToken() { + byte[] tokenBytes = Utf8.encode(this.token); + byte[] randomBytes = new byte[tokenBytes.length]; + this.secureRandom.nextBytes(randomBytes); + + byte[] xoredBytes = xorCsrf(randomBytes, tokenBytes); + byte[] combinedBytes = new byte[tokenBytes.length + randomBytes.length]; + System.arraycopy(randomBytes, 0, combinedBytes, 0, randomBytes.length); + System.arraycopy(xoredBytes, 0, combinedBytes, randomBytes.length, xoredBytes.length); + + return Base64.getUrlEncoder().encodeToString(combinedBytes); + } + + private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { + int len = Math.min(randomBytes.length, csrfBytes.length); + byte[] xoredCsrf = new byte[len]; + System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); + for (int i = 0; i < len; i++) { + xoredCsrf[i] ^= randomBytes[i]; + } + return xoredCsrf; + } + } 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 deleted file mode 100644 index 8c35b3b8369..00000000000 --- a/web/src/main/java/org/springframework/security/web/csrf/GenerateTokenProvider.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 7fe4504663d..44738bec42b 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-2020 the original author or authors. + * Copyright 2002-2022 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,14 +16,15 @@ package org.springframework.security.web.csrf; +import java.security.SecureRandom; import java.util.UUID; -import java.util.function.Function; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * A {@link CsrfTokenRepository} that stores the {@link CsrfToken} in the @@ -47,8 +48,18 @@ 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); + private SecureRandom secureRandom; + + /** + * Creates a new instance of {@link HttpSessionCsrfTokenRepository}. + */ + public HttpSessionCsrfTokenRepository() { + } + + private HttpSessionCsrfTokenRepository(SecureRandom secureRandom) { + Assert.notNull(secureRandom, "secureRandom cannot be null"); + this.secureRandom = secureRandom; + } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { @@ -60,7 +71,7 @@ public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletRe } else { HttpSession session = request.getSession(); - session.setAttribute(this.sessionAttributeName, token); + session.setAttribute(this.sessionAttributeName, getTokenValue(token)); } } @@ -70,21 +81,27 @@ public CsrfToken loadToken(HttpServletRequest request) { if (session == null) { return null; } - return (CsrfToken) session.getAttribute(this.sessionAttributeName); + String token = getTokenValue(session); + if (!StringUtils.hasLength(token)) { + return null; + } + return this.isXorRandomSecretEnabled() + ? new DefaultCsrfToken(this.headerName, this.parameterName, token, this.secureRandom) + : new DefaultCsrfToken(this.headerName, this.parameterName, token); } @Override public CsrfToken generateToken(HttpServletRequest request) { - return generateTokenProvider.apply(createNewToken()); + return this.isXorRandomSecretEnabled() + ? new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken(), this.secureRandom) + : new DefaultCsrfToken(this.headerName, this.parameterName, 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; @@ -94,9 +111,7 @@ public void setParameterName(String parameterName) { * Sets the header name that the {@link CsrfToken} is expected to appear on and the * 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; @@ -111,26 +126,64 @@ public void setSessionAttributeName(String sessionAttributeName) { this.sessionAttributeName = sessionAttributeName; } - private String createNewToken() { - return UUID.randomUUID().toString(); + /** + * Factory method to create an instance of {@link HttpSessionCsrfTokenRepository} that + * has {@link #setXorRandomSecretEnabled(boolean)} set to {@code true}. + * @return an instance of {@link HttpSessionCsrfTokenRepository} with + * {@link #setXorRandomSecretEnabled(boolean)} set to {@code true} + */ + public static HttpSessionCsrfTokenRepository withXorRandomSecretEnabled() { + HttpSessionCsrfTokenRepository result = new HttpSessionCsrfTokenRepository(); + result.setXorRandomSecretEnabled(true); + return result; + } + + /** + * Factory method to create an instance of {@link HttpSessionCsrfTokenRepository} that + * has {@link #setXorRandomSecretEnabled(boolean)} set to {@code true}. + * @param secureRandom the {@link SecureRandom} to use for generating random secrets + * @return an instance of {@link HttpSessionCsrfTokenRepository} with + * {@link #setXorRandomSecretEnabled(boolean)} set to {@code true} + */ + public static HttpSessionCsrfTokenRepository withXorRandomSecretEnabled(SecureRandom secureRandom) { + return new HttpSessionCsrfTokenRepository(secureRandom); } /** - * 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 + * Enables generating random secrets to XOR with the csrf token on each request. + * @param enabled {@code true} sets the {@link SecureRandom} used for generating + * random secrets, {@code false} causes it to be set to null */ - public void setGenerateToken(GenerateTokenProvider generateTokenProvider) { - this.generateTokenProvider = (value) -> generateTokenProvider.generateToken(this.headerName, this.parameterName, - value); + public void setXorRandomSecretEnabled(boolean enabled) { + if (enabled) { + this.secureRandom = new SecureRandom(); + } + else { + this.secureRandom = null; + } + } + + private boolean isXorRandomSecretEnabled() { + return (this.secureRandom != null); + } + + private String getTokenValue(CsrfToken token) { + if (token instanceof DefaultCsrfToken) { + return ((DefaultCsrfToken) token).getRawToken(); + } + return token.getToken(); + } + + private String getTokenValue(HttpSession session) { + Object attributeValue = session.getAttribute(this.sessionAttributeName); + if (attributeValue instanceof CsrfToken) { + return ((CsrfToken) attributeValue).getToken(); + } + return (String) attributeValue; + } + + private String createNewToken() { + return UUID.randomUUID().toString(); } } 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 3618dd4cbca..7e5300387c1 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-2020 the original author or authors. + * Copyright 2012-2022 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. 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 deleted file mode 100644 index 263ec8b4c5c..00000000000 --- a/web/src/main/java/org/springframework/security/web/csrf/XorCsrfToken.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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 0822fc3c437..793872d6d75 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-2020 the original author or authors. + * Copyright 2002-2022 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,61 +69,6 @@ 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); @@ -133,7 +78,7 @@ public void saveToken() { assertThat(tokenCookie.getName()).isEqualTo(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath()); assertThat(tokenCookie.getSecure()).isEqualTo(this.request.isSecure()); - assertThat(token.matches(tokenCookie.getValue())).isTrue(); + assertThat(tokenCookie.getValue()).isEqualTo(token.getToken()); assertThat(tokenCookie.isHttpOnly()).isEqualTo(true); } @@ -298,26 +243,7 @@ public void loadTokenCustom() { assertThat(loadToken).isNotNull(); assertThat(loadToken.getHeaderName()).isEqualTo(headerName); assertThat(loadToken.getParameterName()).isEqualTo(parameterName); - 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(); + assertThat(loadToken.getToken()).isEqualTo(value); } @Test @@ -340,4 +266,82 @@ public void setCookieMaxAgeZeroIllegalArgumentException() { assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieMaxAge(0)); } + @Test + public void withXorRandomSecretEnabledWhenSecureRandomIsNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> CookieCsrfTokenRepository.withXorRandomSecretEnabled(null)) + .withMessage("secureRandom cannot be null"); + } + + @Test + public void withXorRandomSecretEnabledWhenUsedThenReturnsUniqueTokens() { + CookieCsrfTokenRepository repo = CookieCsrfTokenRepository.withXorRandomSecretEnabled(); + + CsrfToken csrfToken = repo.generateToken(this.request); + String token1 = csrfToken.getToken(); + String token2 = csrfToken.getToken(); + assertThat(token1).isNotEqualTo(token2); + assertThat(csrfToken.matches(token1)).isTrue(); + assertThat(csrfToken.matches(token2)).isTrue(); + } + + @Test + public void generateTokenWhenSetXorRandomSecretEnabledTrueThenReturnsUniqueTokens() { + this.repository.setXorRandomSecretEnabled(true); + + CsrfToken csrfToken = this.repository.generateToken(this.request); + String token1 = csrfToken.getToken(); + String token2 = csrfToken.getToken(); + assertThat(token1).isNotEqualTo(token2); + assertThat(csrfToken.matches(token1)).isTrue(); + assertThat(csrfToken.matches(token2)).isTrue(); + } + + @Test + public void generateTokenWhenSetXorRandomSecretEnabledFalseThenReturnsNonUniqueTokens() { + this.repository.setXorRandomSecretEnabled(false); + + CsrfToken csrfToken = this.repository.generateToken(this.request); + String token1 = csrfToken.getToken(); + String token2 = csrfToken.getToken(); + assertThat(token1).isEqualTo(token2); + assertThat(csrfToken.matches(token1)).isTrue(); + assertThat(csrfToken.matches(token2)).isTrue(); + } + + @Test + public void loadTokenWhenSetXorRandomSecretEnabledTrueThenReturnsUniqueTokens() { + CsrfToken generateToken = this.repository.generateToken(this.request); + this.repository.setXorRandomSecretEnabled(true); + this.request + .setCookies(new Cookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, generateToken.getToken())); + + CsrfToken csrfToken = this.repository.loadToken(this.request); + String token1 = csrfToken.getToken(); + String token2 = csrfToken.getToken(); + assertThat(token1).isNotEqualTo(token2); + assertThat(csrfToken.matches(token1)).isTrue(); + assertThat(csrfToken.matches(token2)).isTrue(); + } + + @Test + public void saveTokenWhenSetXorRandomSecretEnabledTrueThenRawTokenIsSaved() { + this.repository.setXorRandomSecretEnabled(true); + + CsrfToken csrfToken = this.repository.generateToken(this.request); + this.repository.saveToken(csrfToken, this.request, this.response); + + Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); + assertThat(tokenCookie.getValue()).isEqualTo(((DefaultCsrfToken) csrfToken).getRawToken()); + } + + @Test + public void matchesWhenSetXorRandomSecretEnabledTrueAndTokensNotEqualThenFalse() { + this.repository.setXorRandomSecretEnabled(true); + + CsrfToken csrfToken1 = this.repository.generateToken(this.request); + CsrfToken csrfToken2 = this.repository.generateToken(this.request); + assertThat(csrfToken1.matches(csrfToken2.getToken())).isFalse(); + } + } 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 f105804e08d..ddfcb031c91 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-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -324,10 +324,7 @@ public void doFilterWhenSkipRequestInvokedThenSkips() throws Exception { @Test public void doFilterWhenTokenIsNullThenNoNullPointer() throws Exception { CsrfFilter filter = createCsrfFilter(this.tokenRepository); - CsrfToken token = mock(CsrfToken.class); - given(token.getToken()).willReturn(null); - given(token.getHeaderName()).willReturn(this.token.getHeaderName()); - given(token.getParameterName()).willReturn(this.token.getParameterName()); + CsrfToken token = new NullCsrfToken(); given(this.tokenRepository.loadToken(this.request)).willReturn(token); given(this.requestMatcher.matches(this.request)).willReturn(true); filter.doFilterInternal(this.request, this.response, this.filterChain); @@ -361,10 +358,30 @@ protected CsrfTokenAssert(CsrfToken actual) { 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; } } + private static final class NullCsrfToken implements CsrfToken { + + @Override + public String getHeaderName() { + return "headerName"; + } + + @Override + public String getParameterName() { + return "paramName"; + } + + @Override + public String getToken() { + return null; + } + + } + } 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 bc5491e1bd7..e2c263c587e 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-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -70,26 +70,29 @@ public void constructorEmptyTokenValue() { } @Test - public void matchesTokenValue() { + public void matchesWhenTokenValuesEqualThenTrue() { 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(); + DefaultCsrfToken token1 = new DefaultCsrfToken(this.headerName, this.parameterName, tokenStr); + DefaultCsrfToken token2 = new DefaultCsrfToken(this.headerName, this.parameterName, tokenStr); + String csrfToken1 = token1.getToken(); + String csrfToken2 = token2.getToken(); + + assertThat(token1.getToken()).isEqualTo(csrfToken1); + assertThat(token2.getToken()).isEqualTo(csrfToken2); + assertThat(csrfToken1).isEqualTo(csrfToken2); + assertThat(token1.matches(csrfToken2)).isTrue(); + assertThat(token2.matches(csrfToken1)).isTrue(); } @Test - public void notMatchesTokenValue() { - DefaultCsrfToken token1 = new DefaultCsrfToken(headerName, parameterName, "token1"); - DefaultCsrfToken token2 = new DefaultCsrfToken(headerName, parameterName, "token2"); + public void matchesWhenTokenValuesNotEqualThenFalse() { + DefaultCsrfToken token1 = new DefaultCsrfToken(this.headerName, this.parameterName, "token1"); + DefaultCsrfToken token2 = new DefaultCsrfToken(this.headerName, this.parameterName, "token2"); String csrfToken1 = token1.getToken(); String csrfToken2 = token2.getToken(); + assertThat(token1.getToken()).isEqualTo(csrfToken1); + assertThat(token2.getToken()).isEqualTo(csrfToken2); 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 5928b42a33e..c84e59a3c6b 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-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -27,7 +27,6 @@ /** * @author Rob Winch - * */ public class HttpSessionCsrfTokenRepositoryTests { @@ -55,61 +54,6 @@ 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"; @@ -145,22 +89,8 @@ public void saveToken() { CsrfToken tokenToSave = new DefaultCsrfToken("123", "abc", "def"); this.repo.saveToken(tokenToSave, this.request, this.response); String attrName = this.request.getSession().getAttributeNames().nextElement(); - CsrfToken loadedToken = (CsrfToken) this.request.getSession().getAttribute(attrName); - 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(); + String loadedToken = (String) this.request.getSession().getAttribute(attrName); + assertThat(loadedToken).isEqualTo(tokenToSave.getToken()); } @Test @@ -169,8 +99,8 @@ public void saveTokenCustomSessionAttribute() { String sessionAttributeName = "custom"; this.repo.setSessionAttributeName(sessionAttributeName); this.repo.saveToken(tokenToSave, this.request, this.response); - CsrfToken loadedToken = (CsrfToken) this.request.getSession().getAttribute(sessionAttributeName); - assertThat(loadedToken).isEqualTo(tokenToSave); + String loadedToken = (String) this.request.getSession().getAttribute(sessionAttributeName); + assertThat(loadedToken).isEqualTo(tokenToSave.getToken()); } @Test @@ -206,4 +136,81 @@ public void setParameterNameNull() { assertThatIllegalArgumentException().isThrownBy(() -> this.repo.setParameterName(null)); } + @Test + public void withXorRandomSecretEnabledWhenSecureRandomIsNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> HttpSessionCsrfTokenRepository.withXorRandomSecretEnabled(null)) + .withMessage("secureRandom cannot be null"); + } + + @Test + public void withXorRandomSecretEnabledWhenUsedThenReturnsUniqueTokens() { + HttpSessionCsrfTokenRepository repo = HttpSessionCsrfTokenRepository.withXorRandomSecretEnabled(); + + CsrfToken csrfToken = repo.generateToken(this.request); + String token1 = csrfToken.getToken(); + String token2 = csrfToken.getToken(); + assertThat(token1).isNotEqualTo(token2); + assertThat(csrfToken.matches(token1)).isTrue(); + assertThat(csrfToken.matches(token2)).isTrue(); + } + + @Test + public void generateTokenWhenSetXorRandomSecretEnabledTrueThenReturnsUniqueTokens() { + this.repo.setXorRandomSecretEnabled(true); + + CsrfToken csrfToken = this.repo.generateToken(this.request); + String token1 = csrfToken.getToken(); + String token2 = csrfToken.getToken(); + assertThat(token1).isNotEqualTo(token2); + assertThat(csrfToken.matches(token1)).isTrue(); + assertThat(csrfToken.matches(token2)).isTrue(); + } + + @Test + public void generateTokenWhenSetXorRandomSecretEnabledFalseThenReturnsNonUniqueTokens() { + this.repo.setXorRandomSecretEnabled(false); + + CsrfToken csrfToken = this.repo.generateToken(this.request); + String token1 = csrfToken.getToken(); + String token2 = csrfToken.getToken(); + assertThat(token1).isEqualTo(token2); + assertThat(csrfToken.matches(token1)).isTrue(); + assertThat(csrfToken.matches(token2)).isTrue(); + } + + @Test + public void loadTokenWhenSetXorRandomSecretEnabledTrueThenReturnsUniqueTokens() { + this.repo.saveToken(new DefaultCsrfToken("123", "abc", "def"), this.request, this.response); + this.repo.setXorRandomSecretEnabled(true); + + CsrfToken csrfToken = this.repo.loadToken(this.request); + String token1 = csrfToken.getToken(); + String token2 = csrfToken.getToken(); + assertThat(token1).isNotEqualTo(token2); + assertThat(csrfToken.matches(token1)).isTrue(); + assertThat(csrfToken.matches(token2)).isTrue(); + } + + @Test + public void saveTokenWhenSetXorRandomSecretEnabledTrueThenRawTokenIsSaved() { + this.repo.setXorRandomSecretEnabled(true); + + CsrfToken csrfToken = this.repo.generateToken(this.request); + this.repo.saveToken(csrfToken, this.request, this.response); + + String sessionAttributeName = this.request.getSession().getAttributeNames().nextElement(); + String tokenValue = (String) this.request.getSession().getAttribute(sessionAttributeName); + assertThat(tokenValue).isEqualTo(((DefaultCsrfToken) csrfToken).getRawToken()); + } + + @Test + public void matchesWhenSetXorRandomSecretEnabledTrueAndTokensNotEqualThenFalse() { + this.repo.setXorRandomSecretEnabled(true); + + CsrfToken csrfToken1 = this.repo.generateToken(this.request); + CsrfToken csrfToken2 = this.repo.generateToken(this.request); + assertThat(csrfToken1.matches(csrfToken2.getToken())).isFalse(); + } + } 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 deleted file mode 100644 index 7d9bffe6d68..00000000000 --- a/web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenTests.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java b/web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java index 78520d218f0..3c9f8a550c1 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 @@ -74,7 +74,9 @@ public void getExtraHiddenFieldsNoCsrfToken() { @Test public void getExtraHiddenFieldsHasCsrfTokenNoMethodSet() { assertThat(this.processor.getExtraHiddenFields(this.request)).isEqualTo(this.expected); - assertThat(token.matches(processor.getExtraHiddenFields(request).get(token.getParameterName()))).isTrue(); + assertThat(this.token + .matches(this.processor.getExtraHiddenFields(this.request).get(this.token.getParameterName()))) + .isTrue(); } @Test @@ -93,14 +95,18 @@ public void getExtraHiddenFieldsHasCsrfToken_get() { public void getExtraHiddenFieldsHasCsrfToken_POST() { this.processor.processAction(this.request, "action", "POST"); assertThat(this.processor.getExtraHiddenFields(this.request)).isEqualTo(this.expected); - assertThat(token.matches(processor.getExtraHiddenFields(request).get(token.getParameterName()))).isTrue(); + assertThat(this.token + .matches(this.processor.getExtraHiddenFields(this.request).get(this.token.getParameterName()))) + .isTrue(); } @Test public void getExtraHiddenFieldsHasCsrfToken_post() { this.processor.processAction(this.request, "action", "post"); assertThat(this.processor.getExtraHiddenFields(this.request)).isEqualTo(this.expected); - assertThat(token.matches(processor.getExtraHiddenFields(request).get(token.getParameterName()))).isTrue(); + assertThat(this.token + .matches(this.processor.getExtraHiddenFields(this.request).get(this.token.getParameterName()))) + .isTrue(); } @Test @@ -135,7 +141,7 @@ public void createGetExtraHiddenFieldsHasCsrfToken() { expected.put(token.getParameterName(), token.getToken()); RequestDataValueProcessor processor = new CsrfRequestDataValueProcessor(); assertThat(processor.getExtraHiddenFields(this.request)).isEqualTo(expected); - assertThat(token.matches(processor.getExtraHiddenFields(request).get(token.getParameterName()))).isTrue(); + assertThat(token.matches(processor.getExtraHiddenFields(this.request).get(token.getParameterName()))).isTrue(); } }