Merge branch '6.3.x'

Closes gh-15187
This commit is contained in:
Steve Riesenberg 2024-05-31 19:02:55 -05:00
commit 8ae7cb175b
No known key found for this signature in database
GPG Key ID: 3D0169B18AB8F0A9
6 changed files with 213 additions and 40 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.security.messaging.web.csrf;
import java.util.Base64; import java.util.Base64;
import org.springframework.security.crypto.codec.Utf8; import org.springframework.security.crypto.codec.Utf8;
import org.springframework.util.Assert;
/** /**
* Copied from * Copied from
@ -43,26 +44,26 @@ final class XorCsrfTokenUtils {
byte[] tokenBytes = Utf8.encode(token); byte[] tokenBytes = Utf8.encode(token);
int tokenSize = tokenBytes.length; int tokenSize = tokenBytes.length;
if (actualBytes.length < tokenSize) { if (actualBytes.length != tokenSize * 2) {
return null; return null;
} }
// extract token and random bytes // extract token and random bytes
int randomBytesSize = actualBytes.length - tokenSize;
byte[] xoredCsrf = new byte[tokenSize]; byte[] xoredCsrf = new byte[tokenSize];
byte[] randomBytes = new byte[randomBytesSize]; byte[] randomBytes = new byte[tokenSize];
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize); System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize); System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf); byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
return Utf8.decode(csrfBytes); return Utf8.decode(csrfBytes);
} }
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
int len = Math.min(randomBytes.length, csrfBytes.length); Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
int len = csrfBytes.length;
byte[] xoredCsrf = new byte[len]; byte[] xoredCsrf = new byte[len];
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
xoredCsrf[i] ^= randomBytes[i]; xoredCsrf[i] ^= randomBytes[i];
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.security.messaging.web.csrf; package org.springframework.security.messaging.web.csrf;
import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -141,6 +142,73 @@ public class XorCsrfChannelInterceptorTests {
this.interceptor.preSend(message(), this.channel); this.interceptor.preSend(message(), this.channel);
} }
// gh-13310, gh-15184
@Test
public void preSendWhenCsrfBytesIsShorterThanRandomBytesThenThrowsInvalidCsrfTokenException() {
/*
* Token format: 3 random pad bytes + 2 padded bytes.
*/
byte[] actualBytes = { 1, 1, 1, 96, 99 };
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
// gh-13310, gh-15184
@Test
public void preSendWhenCsrfBytesIsLongerThanRandomBytesThenThrowsInvalidCsrfTokenException() {
/*
* Token format: 3 random pad bytes + 4 padded bytes.
*/
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
// gh-13310, gh-15184
@Test
public void preSendWhenTokenBytesIsShorterThanActualBytesThenThrowsInvalidCsrfTokenException() {
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "a");
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
// gh-13310, gh-15184
@Test
public void preSendWhenTokenBytesIsLongerThanActualBytesThenThrowsInvalidCsrfTokenException() {
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "abcde");
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
// gh-13310, gh-15184
@Test
public void preSendWhenActualBytesIsEmptyThenThrowsInvalidCsrfTokenException() {
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), "");
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
private Message<String> message() { private Message<String> message() {
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build(); return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -84,20 +84,19 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
byte[] tokenBytes = Utf8.encode(token); byte[] tokenBytes = Utf8.encode(token);
int tokenSize = tokenBytes.length; int tokenSize = tokenBytes.length;
if (actualBytes.length < tokenSize) { if (actualBytes.length != tokenSize * 2) {
return null; return null;
} }
// extract token and random bytes // extract token and random bytes
int randomBytesSize = actualBytes.length - tokenSize;
byte[] xoredCsrf = new byte[tokenSize]; byte[] xoredCsrf = new byte[tokenSize];
byte[] randomBytes = new byte[randomBytesSize]; byte[] randomBytes = new byte[tokenSize];
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize); System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize); System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf); byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null; return Utf8.decode(csrfBytes);
} }
private static String createXoredCsrfToken(SecureRandom secureRandom, String token) { private static String createXoredCsrfToken(SecureRandom secureRandom, String token) {
@ -114,12 +113,10 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
} }
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
if (csrfBytes.length < randomBytes.length) { Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
return null; int len = csrfBytes.length;
}
int len = Math.min(randomBytes.length, csrfBytes.length);
byte[] xoredCsrf = new byte[len]; byte[] xoredCsrf = new byte[len];
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
xoredCsrf[i] ^= randomBytes[i]; xoredCsrf[i] ^= randomBytes[i];
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -77,17 +77,16 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
byte[] tokenBytes = Utf8.encode(token); byte[] tokenBytes = Utf8.encode(token);
int tokenSize = tokenBytes.length; int tokenSize = tokenBytes.length;
if (actualBytes.length < tokenSize) { if (actualBytes.length != tokenSize * 2) {
return null; return null;
} }
// extract token and random bytes // extract token and random bytes
int randomBytesSize = actualBytes.length - tokenSize;
byte[] xoredCsrf = new byte[tokenSize]; byte[] xoredCsrf = new byte[tokenSize];
byte[] randomBytes = new byte[randomBytesSize]; byte[] randomBytes = new byte[tokenSize];
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize); System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize); System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf); byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null; return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null;
@ -107,12 +106,10 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
} }
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
if (csrfBytes.length < randomBytes.length) { Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
return null; int len = csrfBytes.length;
}
int len = Math.min(randomBytes.length, csrfBytes.length);
byte[] xoredCsrf = new byte[len]; byte[] xoredCsrf = new byte[len];
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
xoredCsrf[i] ^= randomBytes[i]; xoredCsrf[i] ^= randomBytes[i];
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -44,6 +44,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*/ */
public class XorCsrfTokenRequestAttributeHandlerTests { public class XorCsrfTokenRequestAttributeHandlerTests {
/*
* Token format: 3 random pad bytes + 3 padded bytes.
*/
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 }; private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES); private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
@ -208,14 +211,58 @@ public class XorCsrfTokenRequestAttributeHandlerTests {
assertThat(tokenValue).isEqualTo(this.token.getToken()); assertThat(tokenValue).isEqualTo(this.token.getToken());
} }
// gh-13310, gh-15184
@Test @Test
public void resolveCsrfTokenIsInvalidThenReturnsNull() { public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
/*
* Token format: 3 random pad bytes + 2 padded bytes.
*/
byte[] actualBytes = { 1, 1, 1, 96, 99 };
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
this.request.setParameter(this.token.getParameterName(), actualToken);
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
assertThat(tokenValue).isNull();
}
// gh-13310, gh-15184
@Test
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
/*
* Token format: 3 random pad bytes + 4 padded bytes.
*/
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
this.request.setParameter(this.token.getParameterName(), actualToken);
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
assertThat(tokenValue).isNull();
}
// gh-13310, gh-15184
@Test
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE); this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a"); CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken); String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
assertThat(tokenValue).isNull(); assertThat(tokenValue).isNull();
} }
// gh-13310, gh-15184
@Test
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
assertThat(tokenValue).isNull();
}
// gh-13310, gh-15184
@Test
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
this.request.setParameter(this.token.getParameterName(), "");
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
assertThat(tokenValue).isNull();
}
private static Answer<Void> fillByteArray() { private static Answer<Void> fillByteArray() {
return (invocation) -> { return (invocation) -> {
byte[] bytes = invocation.getArgument(0); byte[] bytes = invocation.getArgument(0);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -46,6 +46,9 @@ import static org.mockito.Mockito.verify;
*/ */
public class XorServerCsrfTokenRequestAttributeHandlerTests { public class XorServerCsrfTokenRequestAttributeHandlerTests {
/*
* Token format: 3 random pad bytes + 3 padded bytes.
*/
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 }; private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES); private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
@ -188,16 +191,76 @@ public class XorServerCsrfTokenRequestAttributeHandlerTests {
StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete(); StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete();
} }
// gh-13310, gh-15184
@Test @Test
public void resolveCsrfTokenIsInvalidThenReturnsNull() { public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
/*
* Token format: 3 random pad bytes + 2 padded bytes.
*/
byte[] actualBytes = { 1, 1, 1, 96, 99 };
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
this.exchange = MockServerWebExchange this.exchange = MockServerWebExchange
.builder(MockServerHttpRequest.post("/") .builder(MockServerHttpRequest.post("/")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.body(this.token.getParameterName() + "=" + XOR_CSRF_TOKEN_VALUE)) .header(this.token.getHeaderName(), actualToken))
.build(); .build();
CsrfToken token = new DefaultCsrfToken("headerName", "paramName", "a"); String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
Mono<String> csrfToken = this.handler.resolveCsrfTokenValue(this.exchange, token); assertThat(tokenValue).isNull();
assertThat(csrfToken.block()).isNull(); }
// gh-13310, gh-15184
@Test
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
/*
* Token format: 3 random pad bytes + 4 padded bytes.
*/
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
this.exchange = MockServerWebExchange
.builder(MockServerHttpRequest.post("/")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(this.token.getHeaderName(), actualToken))
.build();
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
assertThat(tokenValue).isNull();
}
// gh-13310, gh-15184
@Test
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
this.exchange = MockServerWebExchange
.builder(MockServerHttpRequest.post("/")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
.build();
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
assertThat(tokenValue).isNull();
}
// gh-13310, gh-15184
@Test
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
this.exchange = MockServerWebExchange
.builder(MockServerHttpRequest.post("/")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
.build();
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
assertThat(tokenValue).isNull();
}
// gh-13310, gh-15184
@Test
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
this.exchange = MockServerWebExchange
.builder(MockServerHttpRequest.post("/")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(this.token.getHeaderName(), ""))
.build();
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
assertThat(tokenValue).isNull();
} }
private static Answer<Void> fillByteArray() { private static Answer<Void> fillByteArray() {