7 changed files with 545 additions and 3 deletions
@ -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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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