Skip to content

Commit 9570d0c

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 e7ee703 commit 9570d0c

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.
@@ -17,6 +17,7 @@
1717
package org.springframework.security.web.csrf;
1818

1919
import java.io.IOException;
20+
import java.lang.reflect.Method;
2021
import java.util.Arrays;
2122

2223
import javax.servlet.FilterChain;
@@ -96,6 +97,18 @@ private void resetRequestResponse() {
9697
this.response = new MockHttpServletResponse();
9798
}
9899

100+
@Test
101+
public void nullConstantTimeEquals() throws Exception {
102+
Method method = CsrfFilter.class.getDeclaredMethod("equalsConstantTime", String.class, String.class);
103+
method.setAccessible(true);
104+
assertThat(method.invoke(CsrfFilter.class, null, null)).isEqualTo(true);
105+
String expectedToken = "Hello—World";
106+
String actualToken = new String("Hello—World");
107+
assertThat(method.invoke(CsrfFilter.class, expectedToken, null)).isEqualTo(false);
108+
assertThat(method.invoke(CsrfFilter.class, expectedToken, "hello-world")).isEqualTo(false);
109+
assertThat(method.invoke(CsrfFilter.class, expectedToken, actualToken)).isEqualTo(true);
110+
}
111+
99112
@Test
100113
public void constructorNullRepository() {
101114
assertThatIllegalArgumentException().isThrownBy(() -> 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;
@@ -65,6 +67,18 @@ public class CsrfWebFilterTests {
6567

6668
private MockServerWebExchange post = MockServerWebExchange.from(MockServerHttpRequest.post("/"));
6769

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

0 commit comments

Comments
 (0)