Skip to content

Commit dcb8c56

Browse files
committed
Fix ArrayIndexOutOfBoundsException
Issue gh-13310 Closes gh-15184
1 parent e34621e commit dcb8c56

File tree

6 files changed

+226
-27
lines changed

6 files changed

+226
-27
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

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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,17 +84,16 @@ 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);
10099
return Utf8.decode(csrfBytes);
@@ -114,9 +113,10 @@ private static String createXoredCsrfToken(SecureRandom secureRandom, String tok
114113
}
115114

116115
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
117-
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;
118118
byte[] xoredCsrf = new byte[len];
119-
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
119+
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
120120
for (int i = 0; i < len; i++) {
121121
xoredCsrf[i] ^= randomBytes[i];
122122
}

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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 Utf8.decode(csrfBytes);
@@ -107,9 +106,10 @@ private static String createXoredCsrfToken(SecureRandom secureRandom, String tok
107106
}
108107

109108
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
110-
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;
111111
byte[] xoredCsrf = new byte[len];
112-
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
112+
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
113113
for (int i = 0; i < len; i++) {
114114
xoredCsrf[i] ^= randomBytes[i];
115115
}

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

+56-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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);
@@ -203,6 +206,58 @@ public void resolveCsrfTokenValueWhenHeaderAndParameterSetThenHeaderIsPreferred(
203206
assertThat(tokenValue).isEqualTo(this.token.getToken());
204207
}
205208

209+
// gh-13310, gh-15184
210+
@Test
211+
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
212+
/*
213+
* Token format: 3 random pad bytes + 2 padded bytes.
214+
*/
215+
byte[] actualBytes = { 1, 1, 1, 96, 99 };
216+
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
217+
this.request.setParameter(this.token.getParameterName(), actualToken);
218+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
219+
assertThat(tokenValue).isNull();
220+
}
221+
222+
// gh-13310, gh-15184
223+
@Test
224+
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
225+
/*
226+
* Token format: 3 random pad bytes + 4 padded bytes.
227+
*/
228+
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
229+
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
230+
this.request.setParameter(this.token.getParameterName(), actualToken);
231+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
232+
assertThat(tokenValue).isNull();
233+
}
234+
235+
// gh-13310, gh-15184
236+
@Test
237+
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
238+
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
239+
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
240+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
241+
assertThat(tokenValue).isNull();
242+
}
243+
244+
// gh-13310, gh-15184
245+
@Test
246+
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
247+
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
248+
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
249+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
250+
assertThat(tokenValue).isNull();
251+
}
252+
253+
// gh-13310, gh-15184
254+
@Test
255+
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
256+
this.request.setParameter(this.token.getParameterName(), "");
257+
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
258+
assertThat(tokenValue).isNull();
259+
}
260+
206261
private static Answer<Void> fillByteArray() {
207262
return (invocation) -> {
208263
byte[] bytes = invocation.getArgument(0);

web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java

+76-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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.
@@ -46,6 +46,9 @@
4646
*/
4747
public class XorServerCsrfTokenRequestAttributeHandlerTests {
4848

49+
/*
50+
* Token format: 3 random pad bytes + 3 padded bytes.
51+
*/
4952
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
5053

5154
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
@@ -188,6 +191,78 @@ public void resolveCsrfTokenValueWhenHeaderAndFormDataSetThenFormDataIsPreferred
188191
StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete();
189192
}
190193

194+
// gh-13310, gh-15184
195+
@Test
196+
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
197+
/*
198+
* Token format: 3 random pad bytes + 2 padded bytes.
199+
*/
200+
byte[] actualBytes = { 1, 1, 1, 96, 99 };
201+
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
202+
this.exchange = MockServerWebExchange
203+
.builder(MockServerHttpRequest.post("/")
204+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
205+
.header(this.token.getHeaderName(), actualToken))
206+
.build();
207+
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
208+
assertThat(tokenValue).isNull();
209+
}
210+
211+
// gh-13310, gh-15184
212+
@Test
213+
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
214+
/*
215+
* Token format: 3 random pad bytes + 4 padded bytes.
216+
*/
217+
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
218+
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
219+
this.exchange = MockServerWebExchange
220+
.builder(MockServerHttpRequest.post("/")
221+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
222+
.header(this.token.getHeaderName(), actualToken))
223+
.build();
224+
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
225+
assertThat(tokenValue).isNull();
226+
}
227+
228+
// gh-13310, gh-15184
229+
@Test
230+
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
231+
this.exchange = MockServerWebExchange
232+
.builder(MockServerHttpRequest.post("/")
233+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
234+
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
235+
.build();
236+
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
237+
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
238+
assertThat(tokenValue).isNull();
239+
}
240+
241+
// gh-13310, gh-15184
242+
@Test
243+
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
244+
this.exchange = MockServerWebExchange
245+
.builder(MockServerHttpRequest.post("/")
246+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
247+
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
248+
.build();
249+
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
250+
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
251+
assertThat(tokenValue).isNull();
252+
}
253+
254+
// gh-13310, gh-15184
255+
@Test
256+
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
257+
this.exchange = MockServerWebExchange
258+
.builder(MockServerHttpRequest.post("/")
259+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
260+
.header(this.token.getHeaderName(), ""))
261+
.build();
262+
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
263+
assertThat(tokenValue).isNull();
264+
}
265+
191266
private static Answer<Void> fillByteArray() {
192267
return (invocation) -> {
193268
byte[] bytes = invocation.getArgument(0);

0 commit comments

Comments
 (0)