Skip to content

Commit 22d7043

Browse files
ziqiangaijzheaux
authored andcommitted
Add null check in CsrfFilter and CsrfWebFilter
Solve the problem that CsrfFilter and CsrfWebFilter throws NPE exception when comparing two byte array is equal in low JDK version. When JDK version is lower than 1.8.0_45, method java.security.MessageDigest#isEqual does not verify whether the two arrays are null. And the above two class call this method without null judgment. ZiQiang Zhao<1694392889@qq.com> Closes gh-9561
1 parent 2009b5f commit 22d7043

File tree

4 files changed

+49
-20
lines changed

4 files changed

+49
-20
lines changed

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -174,17 +174,18 @@ public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
174174
* @return
175175
*/
176176
private static boolean equalsConstantTime(String expected, String actual) {
177-
byte[] expectedBytes = bytesUtf8(expected);
178-
byte[] actualBytes = bytesUtf8(actual);
177+
if (expected == actual) {
178+
return true;
179+
}
180+
if (expected == null || actual == null) {
181+
return false;
182+
}
183+
// Encode after ensure that the string is not null
184+
byte[] expectedBytes = Utf8.encode(expected);
185+
byte[] actualBytes = Utf8.encode(actual);
179186
return MessageDigest.isEqual(expectedBytes, actualBytes);
180187
}
181188

182-
private static byte[] bytesUtf8(String s) {
183-
// need to check if Utf8.encode() runs in constant time (probably not).
184-
// This may leak length of string.
185-
return (s != null) ? Utf8.encode(s) : null;
186-
}
187-
188189
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
189190

190191
private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -177,17 +177,18 @@ private Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
177177
* @return
178178
*/
179179
private static boolean equalsConstantTime(String expected, String actual) {
180-
byte[] expectedBytes = bytesUtf8(expected);
181-
byte[] actualBytes = bytesUtf8(actual);
180+
if (expected == actual) {
181+
return true;
182+
}
183+
if (expected == null || actual == null) {
184+
return false;
185+
}
186+
// Encode after ensure that the string is not null
187+
byte[] expectedBytes = Utf8.encode(expected);
188+
byte[] actualBytes = Utf8.encode(actual);
182189
return MessageDigest.isEqual(expectedBytes, actualBytes);
183190
}
184191

185-
private static byte[] bytesUtf8(String s) {
186-
// need to check if Utf8.encode() runs in constant time (probably not).
187-
// This may leak length of string.
188-
return (s != null) ? Utf8.encode(s) : null;
189-
}
190-
191192
private Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
192193
return this.csrfTokenRepository.generateToken(exchange)
193194
.delayUntil((token) -> this.csrfTokenRepository.saveToken(exchange, token));

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2021 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
package org.springframework.security.web.csrf;
1717

1818
import java.io.IOException;
19+
import java.lang.reflect.Method;
1920
import java.util.Arrays;
2021

2122
import javax.servlet.FilterChain;
@@ -89,6 +90,18 @@ private void resetRequestResponse() {
8990
this.response = new MockHttpServletResponse();
9091
}
9192

93+
@Test
94+
public void nullConstantTimeEquals() throws Exception {
95+
Method method = CsrfFilter.class.getDeclaredMethod("equalsConstantTime", String.class, String.class);
96+
method.setAccessible(true);
97+
assertThat(method.invoke(CsrfFilter.class, null, null)).isEqualTo(true);
98+
String expectedToken = "Hello—World";
99+
String actualToken = new String("Hello—World");
100+
assertThat(method.invoke(CsrfFilter.class, expectedToken, null)).isEqualTo(false);
101+
assertThat(method.invoke(CsrfFilter.class, expectedToken, "hello-world")).isEqualTo(false);
102+
assertThat(method.invoke(CsrfFilter.class, expectedToken, actualToken)).isEqualTo(true);
103+
}
104+
92105
@Test(expected = IllegalArgumentException.class)
93106
public void constructorNullRepository() {
94107
new CsrfFilter(null);

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 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,8 @@
1616

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

19+
import java.lang.reflect.Method;
20+
1921
import org.junit.Test;
2022
import org.junit.runner.RunWith;
2123
import org.mockito.Mock;
@@ -67,6 +69,18 @@ public class CsrfWebFilterTests {
6769

6870
private MockServerWebExchange post = MockServerWebExchange.from(MockServerHttpRequest.post("/"));
6971

72+
@Test
73+
public void nullConstantTimeEquals() throws Exception {
74+
Method method = CsrfWebFilter.class.getDeclaredMethod("equalsConstantTime", String.class, String.class);
75+
method.setAccessible(true);
76+
assertThat(method.invoke(CsrfWebFilter.class, null, null)).isEqualTo(true);
77+
String expectedToken = "Hello—World";
78+
String actualToken = new String("Hello—World");
79+
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, null)).isEqualTo(false);
80+
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, "hello-world")).isEqualTo(false);
81+
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, actualToken)).isEqualTo(true);
82+
}
83+
7084
@Test
7185
public void filterWhenGetThenSessionNotCreatedAndChainContinues() {
7286
PublisherProbe<Void> chainResult = PublisherProbe.empty();

0 commit comments

Comments
 (0)