7 changed files with 545 additions and 3 deletions
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!-- |
||||
~ Copyright 2002-2018 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. |
||||
~ You may obtain a copy of the License at |
||||
~ |
||||
~ https://www.apache.org/licenses/LICENSE-2.0 |
||||
~ |
||||
~ Unless required by applicable law or agreed to in writing, software |
||||
~ distributed under the License is distributed on an "AS IS" BASIS, |
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
~ See the License for the specific language governing permissions and |
||||
~ limitations under the License. |
||||
--> |
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans" |
||||
xmlns:p="http://www.springframework.org/schema/p" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xmlns="http://www.springframework.org/schema/security" |
||||
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd |
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> |
||||
|
||||
<http auto-config="true"> |
||||
<csrf request-handler-ref="requestHandler"/> |
||||
</http> |
||||
|
||||
<b:bean id="requestHandler" class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler" |
||||
p:csrfRequestAttributeName="_csrf"/> |
||||
<b:import resource="CsrfConfigTests-shared-userservice.xml"/> |
||||
</b:beans> |
||||
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
/* |
||||
* Copyright 2002-2022 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.csrf; |
||||
|
||||
import java.security.SecureRandom; |
||||
import java.util.Base64; |
||||
import java.util.function.Supplier; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import org.springframework.security.crypto.codec.Utf8; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An implementation of the {@link CsrfTokenRequestHandler} interface that is capable of |
||||
* masking the value of the {@link CsrfToken} on each request and resolving the raw token |
||||
* value from the masked value as either a header or parameter value of the request. |
||||
* |
||||
* @author Steve Riesenberg |
||||
* @since 5.8 |
||||
*/ |
||||
public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestAttributeHandler { |
||||
|
||||
private SecureRandom secureRandom = new SecureRandom(); |
||||
|
||||
/** |
||||
* Specifies the {@code SecureRandom} used to generate random bytes that are used to |
||||
* mask the value of the {@link CsrfToken} on each request. |
||||
* @param secureRandom the {@code SecureRandom} to use to generate random bytes |
||||
*/ |
||||
public void setSecureRandom(SecureRandom secureRandom) { |
||||
Assert.notNull(secureRandom, "secureRandom cannot be null"); |
||||
this.secureRandom = secureRandom; |
||||
} |
||||
|
||||
@Override |
||||
public void handle(HttpServletRequest request, HttpServletResponse response, |
||||
Supplier<CsrfToken> deferredCsrfToken) { |
||||
Assert.notNull(request, "request cannot be null"); |
||||
Assert.notNull(response, "response cannot be null"); |
||||
Assert.notNull(deferredCsrfToken, "deferredCsrfToken cannot be null"); |
||||
Supplier<CsrfToken> updatedCsrfToken = deferCsrfTokenUpdate(deferredCsrfToken); |
||||
super.handle(request, response, updatedCsrfToken); |
||||
} |
||||
|
||||
private Supplier<CsrfToken> deferCsrfTokenUpdate(Supplier<CsrfToken> csrfTokenSupplier) { |
||||
return () -> { |
||||
CsrfToken csrfToken = csrfTokenSupplier.get(); |
||||
Assert.state(csrfToken != null, "csrfToken supplier returned null"); |
||||
String updatedToken = createXoredCsrfToken(this.secureRandom, csrfToken.getToken()); |
||||
return new DefaultCsrfToken(csrfToken.getHeaderName(), csrfToken.getParameterName(), updatedToken); |
||||
}; |
||||
} |
||||
|
||||
@Override |
||||
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { |
||||
String actualToken = super.resolveCsrfTokenValue(request, csrfToken); |
||||
return getTokenValue(actualToken, csrfToken.getToken()); |
||||
} |
||||
|
||||
private static String getTokenValue(String actualToken, String token) { |
||||
byte[] actualBytes; |
||||
try { |
||||
actualBytes = Base64.getUrlDecoder().decode(actualToken); |
||||
} |
||||
catch (Exception ex) { |
||||
return null; |
||||
} |
||||
|
||||
byte[] tokenBytes = Utf8.encode(token); |
||||
int tokenSize = tokenBytes.length; |
||||
if (actualBytes.length < tokenSize) { |
||||
return null; |
||||
} |
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize; |
||||
byte[] xoredCsrf = new byte[tokenSize]; |
||||
byte[] randomBytes = new byte[randomBytesSize]; |
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize); |
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize); |
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf); |
||||
return Utf8.decode(csrfBytes); |
||||
} |
||||
|
||||
private static String createXoredCsrfToken(SecureRandom secureRandom, String token) { |
||||
byte[] tokenBytes = Utf8.encode(token); |
||||
byte[] randomBytes = new byte[tokenBytes.length]; |
||||
secureRandom.nextBytes(randomBytes); |
||||
|
||||
byte[] xoredBytes = xorCsrf(randomBytes, tokenBytes); |
||||
byte[] combinedBytes = new byte[tokenBytes.length + randomBytes.length]; |
||||
System.arraycopy(randomBytes, 0, combinedBytes, 0, randomBytes.length); |
||||
System.arraycopy(xoredBytes, 0, combinedBytes, randomBytes.length, xoredBytes.length); |
||||
|
||||
return Base64.getUrlEncoder().encodeToString(combinedBytes); |
||||
} |
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { |
||||
int len = Math.min(randomBytes.length, csrfBytes.length); |
||||
byte[] xoredCsrf = new byte[len]; |
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); |
||||
for (int i = 0; i < len; i++) { |
||||
xoredCsrf[i] ^= randomBytes[i]; |
||||
} |
||||
return xoredCsrf; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,203 @@
@@ -0,0 +1,203 @@
|
||||
/* |
||||
* Copyright 2002-2022 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.csrf; |
||||
|
||||
import java.security.SecureRandom; |
||||
import java.util.Arrays; |
||||
import java.util.Base64; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.stubbing.Answer; |
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.mock.web.MockHttpServletResponse; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.BDDMockito.willAnswer; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||
|
||||
/** |
||||
* Tests for {@link XorCsrfTokenRequestAttributeHandler}. |
||||
* |
||||
* @author Steve Riesenberg |
||||
* @since 5.8 |
||||
*/ |
||||
public class XorCsrfTokenRequestAttributeHandlerTests { |
||||
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 }; |
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES); |
||||
|
||||
private MockHttpServletRequest request; |
||||
|
||||
private MockHttpServletResponse response; |
||||
|
||||
private CsrfToken token; |
||||
|
||||
private SecureRandom secureRandom; |
||||
|
||||
private XorCsrfTokenRequestAttributeHandler handler; |
||||
|
||||
@BeforeEach |
||||
public void setup() { |
||||
this.request = new MockHttpServletRequest(); |
||||
this.response = new MockHttpServletResponse(); |
||||
this.token = new DefaultCsrfToken("headerName", "paramName", "abc"); |
||||
this.secureRandom = mock(SecureRandom.class); |
||||
this.handler = new XorCsrfTokenRequestAttributeHandler(); |
||||
} |
||||
|
||||
@Test |
||||
public void setSecureRandomWhenNullThenThrowsIllegalArgumentException() { |
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.handler.setSecureRandom(null)) |
||||
.withMessage("secureRandom cannot be null"); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void handleWhenRequestIsNullThenThrowsIllegalArgumentException() { |
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.handler.handle(null, this.response, () -> this.token)) |
||||
.withMessage("request cannot be null"); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void handleWhenResponseIsNullThenThrowsIllegalArgumentException() { |
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.handler.handle(this.request, null, () -> this.token)) |
||||
.withMessage("response cannot be null"); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void handleWhenCsrfTokenSupplierIsNullThenThrowsIllegalArgumentException() { |
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.handler.handle(this.request, this.response, null)) |
||||
.withMessage("deferredCsrfToken cannot be null"); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void handleWhenCsrfTokenIsNullThenThrowsIllegalStateException() { |
||||
// @formatter:off
|
||||
assertThatIllegalStateException() |
||||
.isThrownBy(() -> this.handler.handle(this.request, this.response, () -> null)) |
||||
.withMessage("csrfToken supplier returned null"); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void handleWhenCsrfRequestAttributeSetThenUsed() { |
||||
willAnswer(fillByteArray()).given(this.secureRandom).nextBytes(anyByteArray()); |
||||
|
||||
this.handler.setSecureRandom(this.secureRandom); |
||||
this.handler.setCsrfRequestAttributeName("_csrf"); |
||||
this.handler.handle(this.request, this.response, () -> this.token); |
||||
assertThat(this.request.getAttribute(CsrfToken.class.getName())).isNotNull(); |
||||
assertThat(this.request.getAttribute("_csrf")).isNotNull(); |
||||
|
||||
CsrfToken csrfTokenAttribute = (CsrfToken) this.request.getAttribute("_csrf"); |
||||
assertThat(csrfTokenAttribute.getToken()).isEqualTo(XOR_CSRF_TOKEN_VALUE); |
||||
} |
||||
|
||||
@Test |
||||
public void handleWhenSecureRandomSetThenUsed() { |
||||
this.handler.setSecureRandom(this.secureRandom); |
||||
this.handler.handle(this.request, this.response, () -> this.token); |
||||
verify(this.secureRandom).nextBytes(anyByteArray()); |
||||
verifyNoMoreInteractions(this.secureRandom); |
||||
} |
||||
|
||||
@Test |
||||
public void handleWhenValidParametersThenRequestAttributesSet() { |
||||
willAnswer(fillByteArray()).given(this.secureRandom).nextBytes(anyByteArray()); |
||||
|
||||
this.handler.setSecureRandom(this.secureRandom); |
||||
this.handler.handle(this.request, this.response, () -> this.token); |
||||
verify(this.secureRandom).nextBytes(anyByteArray()); |
||||
assertThat(this.request.getAttribute(CsrfToken.class.getName())).isNotNull(); |
||||
assertThat(this.request.getAttribute(this.token.getParameterName())).isNotNull(); |
||||
|
||||
CsrfToken csrfTokenAttribute = (CsrfToken) this.request.getAttribute(CsrfToken.class.getName()); |
||||
assertThat(csrfTokenAttribute.getToken()).isEqualTo(XOR_CSRF_TOKEN_VALUE); |
||||
} |
||||
|
||||
@Test |
||||
public void resolveCsrfTokenValueWhenRequestIsNullThenThrowsIllegalArgumentException() { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.resolveCsrfTokenValue(null, this.token)) |
||||
.withMessage("request cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void resolveCsrfTokenValueWhenCsrfTokenIsNullThenThrowsIllegalArgumentException() { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.resolveCsrfTokenValue(this.request, null)) |
||||
.withMessage("csrfToken cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void resolveCsrfTokenValueWhenTokenNotSetThenReturnsNull() { |
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token); |
||||
assertThat(tokenValue).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void resolveCsrfTokenValueWhenParameterSetThenReturnsTokenValue() { |
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE); |
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token); |
||||
assertThat(tokenValue).isEqualTo(this.token.getToken()); |
||||
} |
||||
|
||||
@Test |
||||
public void resolveCsrfTokenValueWhenHeaderSetThenReturnsTokenValue() { |
||||
this.request.addHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); |
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token); |
||||
assertThat(tokenValue).isEqualTo(this.token.getToken()); |
||||
} |
||||
|
||||
@Test |
||||
public void resolveCsrfTokenValueWhenHeaderAndParameterSetThenHeaderIsPreferred() { |
||||
this.request.addHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); |
||||
this.request.setParameter(this.token.getParameterName(), "invalid"); |
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token); |
||||
assertThat(tokenValue).isEqualTo(this.token.getToken()); |
||||
} |
||||
|
||||
private static Answer<Void> fillByteArray() { |
||||
return (invocation) -> { |
||||
byte[] bytes = invocation.getArgument(0); |
||||
Arrays.fill(bytes, (byte) 1); |
||||
return null; |
||||
}; |
||||
} |
||||
|
||||
private static byte[] anyByteArray() { |
||||
return any(byte[].class); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue