Skip to content

Commit

Permalink
Add null check in CsrfFilter and CsrfWebFilter
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ziqiangai authored and jzheaux committed Apr 10, 2021
1 parent 2009b5f commit 22d7043
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 20 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -174,17 +174,18 @@ public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
* @return
*/
private static boolean equalsConstantTime(String expected, String actual) {
byte[] expectedBytes = bytesUtf8(expected);
byte[] actualBytes = bytesUtf8(actual);
if (expected == actual) {
return true;
}
if (expected == null || actual == null) {
return false;
}
// Encode after ensure that the string is not null
byte[] expectedBytes = Utf8.encode(expected);
byte[] actualBytes = Utf8.encode(actual);
return MessageDigest.isEqual(expectedBytes, actualBytes);
}

private static byte[] bytesUtf8(String s) {
// need to check if Utf8.encode() runs in constant time (probably not).
// This may leak length of string.
return (s != null) ? Utf8.encode(s) : null;
}

private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {

private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -177,17 +177,18 @@ private Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
* @return
*/
private static boolean equalsConstantTime(String expected, String actual) {
byte[] expectedBytes = bytesUtf8(expected);
byte[] actualBytes = bytesUtf8(actual);
if (expected == actual) {
return true;
}
if (expected == null || actual == null) {
return false;
}
// Encode after ensure that the string is not null
byte[] expectedBytes = Utf8.encode(expected);
byte[] actualBytes = Utf8.encode(actual);
return MessageDigest.isEqual(expectedBytes, actualBytes);
}

private static byte[] bytesUtf8(String s) {
// need to check if Utf8.encode() runs in constant time (probably not).
// This may leak length of string.
return (s != null) ? Utf8.encode(s) : null;
}

private Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
return this.csrfTokenRepository.generateToken(exchange)
.delayUntil((token) -> this.csrfTokenRepository.saveToken(exchange, token));
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,7 @@
package org.springframework.security.web.csrf;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;

import javax.servlet.FilterChain;
Expand Down Expand Up @@ -89,6 +90,18 @@ private void resetRequestResponse() {
this.response = new MockHttpServletResponse();
}

@Test
public void nullConstantTimeEquals() throws Exception {
Method method = CsrfFilter.class.getDeclaredMethod("equalsConstantTime", String.class, String.class);
method.setAccessible(true);
assertThat(method.invoke(CsrfFilter.class, null, null)).isEqualTo(true);
String expectedToken = "Hello—World";
String actualToken = new String("Hello—World");
assertThat(method.invoke(CsrfFilter.class, expectedToken, null)).isEqualTo(false);
assertThat(method.invoke(CsrfFilter.class, expectedToken, "hello-world")).isEqualTo(false);
assertThat(method.invoke(CsrfFilter.class, expectedToken, actualToken)).isEqualTo(true);
}

@Test(expected = IllegalArgumentException.class)
public void constructorNullRepository() {
new CsrfFilter(null);
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,8 @@

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

import java.lang.reflect.Method;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
Expand Down Expand Up @@ -67,6 +69,18 @@ public class CsrfWebFilterTests {

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

@Test
public void nullConstantTimeEquals() throws Exception {
Method method = CsrfWebFilter.class.getDeclaredMethod("equalsConstantTime", String.class, String.class);
method.setAccessible(true);
assertThat(method.invoke(CsrfWebFilter.class, null, null)).isEqualTo(true);
String expectedToken = "Hello—World";
String actualToken = new String("Hello—World");
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, null)).isEqualTo(false);
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, "hello-world")).isEqualTo(false);
assertThat(method.invoke(CsrfWebFilter.class, expectedToken, actualToken)).isEqualTo(true);
}

@Test
public void filterWhenGetThenSessionNotCreatedAndChainContinues() {
PublisherProbe<Void> chainResult = PublisherProbe.empty();
Expand Down

0 comments on commit 22d7043

Please sign in to comment.