Skip to content

Commit 1e4aff2

Browse files
committed
Merge branch '6.2.x' into 6.3.x
Closes gh-15186
2 parents 3defed4 + 3fc7b6e commit 1e4aff2

File tree

6 files changed

+213
-40
lines changed

6 files changed

+213
-40
lines changed

messaging/src/main/java/org/springframework/security/messaging/web/csrf/XorCsrfTokenUtils.java

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.Base64;
2020

2121
import org.springframework.security.crypto.codec.Utf8;
22+
import org.springframework.util.Assert;
2223

2324
/**
2425
* Copied from
@@ -43,26 +44,26 @@ static String getTokenValue(String actualToken, String token) {
4344

4445
byte[] tokenBytes = Utf8.encode(token);
4546
int tokenSize = tokenBytes.length;
46-
if (actualBytes.length < tokenSize) {
47+
if (actualBytes.length != tokenSize * 2) {
4748
return null;
4849
}
4950

5051
// extract token and random bytes
51-
int randomBytesSize = actualBytes.length - tokenSize;
5252
byte[] xoredCsrf = new byte[tokenSize];
53-
byte[] randomBytes = new byte[randomBytesSize];
53+
byte[] randomBytes = new byte[tokenSize];
5454

55-
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
56-
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
55+
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
56+
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
5757

5858
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
5959
return Utf8.decode(csrfBytes);
6060
}
6161

6262
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
63-
int len = Math.min(randomBytes.length, csrfBytes.length);
63+
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
64+
int len = csrfBytes.length;
6465
byte[] xoredCsrf = new byte[len];
65-
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
66+
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
6667
for (int i = 0; i < len; i++) {
6768
xoredCsrf[i] ^= randomBytes[i];
6869
}

messaging/src/test/java/org/springframework/security/messaging/web/csrf/XorCsrfChannelInterceptorTests.java

+69-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.messaging.web.csrf;
1818

19+
import java.util.Base64;
1920
import java.util.HashMap;
2021

2122
import org.junit.jupiter.api.BeforeEach;
@@ -141,6 +142,73 @@ public void preSendWhenUnsubscribeThenIgnores() {
141142
this.interceptor.preSend(message(), this.channel);
142143
}
143144

145+
// gh-13310, gh-15184
146+
@Test
147+
public void preSendWhenCsrfBytesIsShorterThanRandomBytesThenThrowsInvalidCsrfTokenException() {
148+
/*
149+
* Token format: 3 random pad bytes + 2 padded bytes.
150+
*/
151+
byte[] actualBytes = { 1, 1, 1, 96, 99 };
152+
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
153+
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
154+
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
155+
// @formatter:off
156+
assertThatExceptionOfType(InvalidCsrfTokenException.class)
157+
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
158+
// @formatter:on
159+
}
160+
161+
// gh-13310, gh-15184
162+
@Test
163+
public void preSendWhenCsrfBytesIsLongerThanRandomBytesThenThrowsInvalidCsrfTokenException() {
164+
/*
165+
* Token format: 3 random pad bytes + 4 padded bytes.
166+
*/
167+
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
168+
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
169+
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
170+
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
171+
// @formatter:off
172+
assertThatExceptionOfType(InvalidCsrfTokenException.class)
173+
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
174+
// @formatter:on
175+
}
176+
177+
// gh-13310, gh-15184
178+
@Test
179+
public void preSendWhenTokenBytesIsShorterThanActualBytesThenThrowsInvalidCsrfTokenException() {
180+
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
181+
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "a");
182+
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
183+
// @formatter:off
184+
assertThatExceptionOfType(InvalidCsrfTokenException.class)
185+
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
186+
// @formatter:on
187+
}
188+
189+
// gh-13310, gh-15184
190+
@Test
191+
public void preSendWhenTokenBytesIsLongerThanActualBytesThenThrowsInvalidCsrfTokenException() {
192+
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
193+
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "abcde");
194+
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
195+
// @formatter:off
196+
assertThatExceptionOfType(InvalidCsrfTokenException.class)
197+
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
198+
// @formatter:on
199+
}
200+
201+
// gh-13310, gh-15184
202+
@Test
203+
public void preSendWhenActualBytesIsEmptyThenThrowsInvalidCsrfTokenException() {
204+
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), "");
205+
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
206+
// @formatter:off
207+
assertThatExceptionOfType(InvalidCsrfTokenException.class)
208+
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
209+
// @formatter:on
210+
}
211+
144212
private Message<String> message() {
145213
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
146214
}

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

+9-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -84,20 +84,19 @@ private static String getTokenValue(String actualToken, String token) {
8484

8585
byte[] tokenBytes = Utf8.encode(token);
8686
int tokenSize = tokenBytes.length;
87-
if (actualBytes.length < tokenSize) {
87+
if (actualBytes.length != tokenSize * 2) {
8888
return null;
8989
}
9090

9191
// extract token and random bytes
92-
int randomBytesSize = actualBytes.length - tokenSize;
9392
byte[] xoredCsrf = new byte[tokenSize];
94-
byte[] randomBytes = new byte[randomBytesSize];
93+
byte[] randomBytes = new byte[tokenSize];
9594

96-
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
97-
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
95+
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
96+
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
9897

9998
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
100-
return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null;
99+
return Utf8.decode(csrfBytes);
101100
}
102101

103102
private static String createXoredCsrfToken(SecureRandom secureRandom, String token) {
@@ -114,12 +113,10 @@ private static String createXoredCsrfToken(SecureRandom secureRandom, String tok
114113
}
115114

116115
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
117-
if (csrfBytes.length < randomBytes.length) {
118-
return null;
119-
}
120-
int len = Math.min(randomBytes.length, csrfBytes.length);
116+
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
117+
int len = csrfBytes.length;
121118
byte[] xoredCsrf = new byte[len];
122-
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
119+
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
123120
for (int i = 0; i < len; i++) {
124121
xoredCsrf[i] ^= randomBytes[i];
125122
}

web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -77,17 +77,16 @@ private static String getTokenValue(String actualToken, String token) {
7777

7878
byte[] tokenBytes = Utf8.encode(token);
7979
int tokenSize = tokenBytes.length;
80-
if (actualBytes.length < tokenSize) {
80+
if (actualBytes.length != tokenSize * 2) {
8181
return null;
8282
}
8383

8484
// extract token and random bytes
85-
int randomBytesSize = actualBytes.length - tokenSize;
8685
byte[] xoredCsrf = new byte[tokenSize];
87-
byte[] randomBytes = new byte[randomBytesSize];
86+
byte[] randomBytes = new byte[tokenSize];
8887

89-
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
90-
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
88+
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
89+
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
9190

9291
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
9392
return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null;
@@ -107,12 +106,10 @@ private static String createXoredCsrfToken(SecureRandom secureRandom, String tok
107106
}
108107

109108
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
110-
if (csrfBytes.length < randomBytes.length) {
111-
return null;
112-
}
113-
int len = Math.min(randomBytes.length, csrfBytes.length);
109+
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
110+
int len = csrfBytes.length;
114111
byte[] xoredCsrf = new byte[len];
115-
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
112+
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
116113
for (int i = 0; i < len; i++) {
117114
xoredCsrf[i] ^= randomBytes[i];
118115
}

web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandlerTests.java

+49-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -44,6 +44,9 @@
4444
*/
4545
public class XorCsrfTokenRequestAttributeHandlerTests {
4646

47+
/*
48+
* Token format: 3 random pad bytes + 3 padded bytes.
49+
*/
4750
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
4851

4952
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
@@ -208,14 +211,58 @@ public void resolveCsrfTokenValueWhenHeaderAndParameterSetThenHeaderIsPreferred(
208211
assertThat(tokenValue).isEqualTo(this.token.getToken());
209212
}
210213

214+
// gh-13310, gh-15184
211215
@Test
212-
public void resolveCsrfTokenIsInvalidThenReturnsNull() {
216+
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
217+
/*
218+
* Token format: 3 random pad bytes + 2 padded bytes.
219+
*/
220+
byte[] actualBytes = { 1, 1, 1, 96, 99 };
221+
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
222+
this.request.setParameter(this.token.getParameterName(), actualToken);
223+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
224+
assertThat(tokenValue).isNull();
225+
}
226+
227+
// gh-13310, gh-15184
228+
@Test
229+
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
230+
/*
231+
* Token format: 3 random pad bytes + 4 padded bytes.
232+
*/
233+
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
234+
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
235+
this.request.setParameter(this.token.getParameterName(), actualToken);
236+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
237+
assertThat(tokenValue).isNull();
238+
}
239+
240+
// gh-13310, gh-15184
241+
@Test
242+
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
213243
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
214244
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
215245
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
216246
assertThat(tokenValue).isNull();
217247
}
218248

249+
// gh-13310, gh-15184
250+
@Test
251+
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
252+
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
253+
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
254+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
255+
assertThat(tokenValue).isNull();
256+
}
257+
258+
// gh-13310, gh-15184
259+
@Test
260+
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
261+
this.request.setParameter(this.token.getParameterName(), "");
262+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
263+
assertThat(tokenValue).isNull();
264+
}
265+
219266
private static Answer<Void> fillByteArray() {
220267
return (invocation) -> {
221268
byte[] bytes = invocation.getArgument(0);

0 commit comments

Comments
 (0)