Skip to content

Fix Issue 4001: CSRF tokens are vulnerable to a BREACH attack #4002

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void deprecatedAuthenticationPrincipalResolved() throws Exception {

@Test
public void csrfToken() throws Exception {
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "token");
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName");
MockHttpServletRequestBuilder request = get("/csrf").requestAttr(
CsrfToken.class.getName(), csrfToken);

Expand Down Expand Up @@ -132,4 +132,4 @@ public TestController testController() {
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerDocTests {

@Before
public void setup() {
token = new DefaultCsrfToken("header", "param", "token");
token = new DefaultCsrfToken("header", "param");
sessionAttr = "sessionAttr";
messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {

@Before
public void setup() {
token = new DefaultCsrfToken("header", "param", "token");
token = new DefaultCsrfToken("header", "param");
sessionAttr = "sessionAttr";
messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
String actualTokenValue = SimpMessageHeaderAccessor.wrap(message)
.getFirstNativeHeader(expectedToken.getHeaderName());

boolean csrfCheckPassed = expectedToken.getToken().equals(actualTokenValue);
boolean csrfCheckPassed = expectedToken.isValid(actualTokenValue);
if (csrfCheckPassed) {
return message;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public class CsrfChannelInterceptorTests {

@Before
public void setup() {
token = new DefaultCsrfToken("header", "param", "token");
token = new DefaultCsrfToken("header", "param");
interceptor = new CsrfChannelInterceptor();

messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Expand Down Expand Up @@ -126,7 +126,7 @@ public void preSendNoToken() {
@Test(expected = InvalidCsrfTokenException.class)
public void preSendInvalidToken() {
messageHeaders.setNativeHeader(token.getHeaderName(), token.getToken()
+ "invalid");
.replaceAll("^.{10}","INVALID000"));

interceptor.preSend(message(), channel);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void beforeHandshakeNoAttribute() throws Exception {

@Test
public void beforeHandshake() throws Exception {
CsrfToken token = new DefaultCsrfToken("header", "param", "token");
CsrfToken token = new DefaultCsrfToken("header", "param");
httpRequest.setAttribute(CsrfToken.class.getName(), token);

interceptor.beforeHandshake(request, response, wsHandler, attributes);
Expand All @@ -79,4 +79,4 @@ public void beforeHandshake() throws Exception {
assertThat(attributes.values()).containsOnly(token);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ public void defaults() throws Exception {
assertThat(request.getParameter("username")).isEqualTo("user");
assertThat(request.getParameter("password")).isEqualTo("password");
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName()))
.isEqualTo(token.getToken());
assertThat(token.isValid(request.getParameter(token.getParameterName())));
assertThat(request.getRequestURI()).isEqualTo("/login");
assertThat(request.getParameter("_csrf")).isNotNull();
}
Expand All @@ -61,8 +60,7 @@ public void custom() throws Exception {
assertThat(request.getParameter("username")).isEqualTo("admin");
assertThat(request.getParameter("password")).isEqualTo("secret");
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName()))
.isEqualTo(token.getToken());
assertThat(token.isValid(request.getParameter(token.getParameterName())));
assertThat(request.getRequestURI()).isEqualTo("/login");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public void defaults() throws Exception {
CsrfToken token = (CsrfToken) request.getAttribute(CsrfRequestPostProcessor.TestCsrfTokenRepository.ATTR_NAME);

assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName())).isEqualTo(
token.getToken());
assertThat(token.isValid(request.getParameter(token.getParameterName())));
assertThat(request.getRequestURI()).isEqualTo("/logout");
}

Expand All @@ -53,8 +52,7 @@ public void custom() throws Exception {
CsrfToken token = (CsrfToken) request.getAttribute(CsrfRequestPostProcessor.TestCsrfTokenRepository.ATTR_NAME);

assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName())).isEqualTo(
token.getToken());
assertThat(token.isValid(request.getParameter(token.getParameterName())));
assertThat(request.getRequestURI()).isEqualTo("/admin/logout");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ public CookieCsrfTokenRepository() {

@Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
return new DefaultCsrfToken(this.headerName, this.parameterName);
}

@Override
Expand Down Expand Up @@ -96,7 +95,7 @@ public CsrfToken loadToken(HttpServletRequest request) {
if (!StringUtils.hasLength(token)) {
return null;
}
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
return new DefaultCsrfToken(this.headerName, this.parameterName);
}

/**
Expand Down Expand Up @@ -165,8 +164,4 @@ public static CookieCsrfTokenRepository withHttpOnlyFalse() {
result.setCookieHttpOnly(false);
return result;
}

private String createNewToken() {
return UUID.randomUUID().toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ protected void doFilterInternal(HttpServletRequest request,
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (!csrfToken.isValid(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* @see DefaultCsrfToken
*
* @author Rob Winch
* @author John Ray
* @since 3.2
*
*/
Expand All @@ -49,4 +50,9 @@ public interface CsrfToken extends Serializable {
*/
String getToken();

}
/**
* Check if a value returned by a previous call to getToken() matches this token.
* @return true if the token is Valid
*/
boolean isValid(String value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,42 @@
*/
package org.springframework.security.web.csrf;

import org.springframework.security.crypto.codec.Base64;
import org.springframework.util.Assert;

import java.security.SecureRandom;

/**
* A CSRF token that is used to protect against CSRF attacks.
*
* @author Rob Winch
* @author John Ray
* @since 3.2
*/
@SuppressWarnings("serial")
public final class DefaultCsrfToken implements CsrfToken {

private final String token;
private static final int CSRF_VALUE_SIZE = 16; // 128 bit CSRF value

private static final SecureRandom secureRandom = new SecureRandom();

private final String parameterName;

private final String headerName;

private final byte[] csrfToken;

/**
* 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 DefaultCsrfToken(String headerName, String parameterName, String token) {
public DefaultCsrfToken(String headerName, String parameterName) {
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.token = token;
csrfToken = secureRandom.generateSeed(CSRF_VALUE_SIZE);
}

/*
Expand All @@ -54,7 +59,7 @@ public DefaultCsrfToken(String headerName, String parameterName, String token) {
* @see org.springframework.security.web.csrf.CsrfToken#getHeaderName()
*/
public String getHeaderName() {
return this.headerName;
return headerName;
}

/*
Expand All @@ -63,15 +68,54 @@ public String getHeaderName() {
* @see org.springframework.security.web.csrf.CsrfToken#getParameterName()
*/
public String getParameterName() {
return this.parameterName;
return parameterName;
}

/*
* (non-Javadoc)
/**
* Get the CSRF token value. Each call to this method will return a unique
* value to defeat a possible BREACH attack.
*
* <p>The value consists of a 128 bit random mask followed by a 128 bit token
* XORed against the mask. The value is Base64 encoded.
*
* @see org.springframework.security.web.csrf.CsrfToken#getToken()
* @return A unique CSRF token.
*/
public String getToken() {
return this.token;
byte[] encodedToken = new byte[CSRF_VALUE_SIZE*2];

byte[] mask = secureRandom.generateSeed(CSRF_VALUE_SIZE);
for (int i=0; i < CSRF_VALUE_SIZE; i++) {
encodedToken[i] = mask[i];
encodedToken[i+CSRF_VALUE_SIZE] = (byte)(csrfToken[i] ^ mask[i]);
}

return new String(Base64.encode(encodedToken));
}

/*
* (non-Javadoc)
*
* @see org.springframework.security.web.csrf.CsrfToken#isValid()
*/
public boolean isValid(String value) {
if ((value == null) || (value.length() == 0))
return false;

byte[] encodedToken;
try {
encodedToken = Base64.decode(value.getBytes());
} catch (IllegalArgumentException e) {
return false;
}

if (encodedToken.length != (CSRF_VALUE_SIZE*2))
return false;

for (int i=0; i < CSRF_VALUE_SIZE; i++)
if (csrfToken[i] != (encodedToken[i] ^ encodedToken[i+CSRF_VALUE_SIZE]))
return false;

return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package org.springframework.security.web.csrf;

import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
Expand Down Expand Up @@ -87,8 +85,7 @@ public CsrfToken loadToken(HttpServletRequest request) {
* servlet .http.HttpServletRequest)
*/
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
return new DefaultCsrfToken(this.headerName, this.parameterName);
}

/**
Expand Down Expand Up @@ -122,7 +119,4 @@ public void setSessionAttributeName(String sessionAttributeName) {
this.sessionAttributeName = sessionAttributeName;
}

private String createNewToken() {
return UUID.randomUUID().toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ public String getToken() {
return this.delegate.getToken();
}

@Override
public boolean isValid(String value) {
return delegate.isValid(value);
}

@Override
public String toString() {
return "SaveOnAccessCsrfToken [delegate=" + this.delegate + "]";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public void saveToken() {
.isEqualTo(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
assertThat(tokenCookie.getSecure()).isEqualTo(this.request.isSecure());
assertThat(tokenCookie.getValue()).isEqualTo(token.getToken());
assertThat(token.isValid(tokenCookie.getValue()));
assertThat(tokenCookie.isHttpOnly()).isEqualTo(true);
}

Expand Down Expand Up @@ -204,7 +204,7 @@ public void loadTokenCustom() {
assertThat(loadToken).isNotNull();
assertThat(loadToken.getHeaderName()).isEqualTo(headerName);
assertThat(loadToken.getParameterName()).isEqualTo(parameterName);
assertThat(loadToken.getToken()).isEqualTo(value);
assertThat(loadToken.isValid(value));
}

@Test(expected = IllegalArgumentException.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public void setup() {
this.request = new MockHttpServletRequest();
this.request.setAttribute(HttpServletResponse.class.getName(), this.response);
this.strategy = new CsrfAuthenticationStrategy(this.csrfTokenRepository);
this.existingToken = new DefaultCsrfToken("_csrf", "_csrf", "1");
this.generatedToken = new DefaultCsrfToken("_csrf", "_csrf", "2");
this.existingToken = new DefaultCsrfToken("_csrf", "_csrf");
this.generatedToken = new DefaultCsrfToken("_csrf", "_csrf");
}

@Test(expected = IllegalArgumentException.class)
Expand All @@ -85,7 +85,7 @@ public void logoutRemovesCsrfTokenAndSavesNew() {
// SEC-2404, SEC-2832
CsrfToken tokenInRequest = (CsrfToken) this.request
.getAttribute(CsrfToken.class.getName());
assertThat(tokenInRequest.getToken()).isSameAs(this.generatedToken.getToken());
assertThat(tokenInRequest.isValid(this.generatedToken.getToken()));
assertThat(tokenInRequest.getHeaderName())
.isSameAs(this.generatedToken.getHeaderName());
assertThat(tokenInRequest.getParameterName())
Expand Down
Loading