Add XorCsrfChannelInterceptor

Issue gh-12378
This commit is contained in:
Steve Riesenberg
2023-01-09 15:58:28 -06:00
parent d42405de42
commit c306df9b46
12 changed files with 501 additions and 58 deletions

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2002-2023 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.messaging.web.csrf;
import java.util.HashMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link XorCsrfChannelInterceptor}.
*
* @author Steve Riesenberg
*/
public class XorCsrfChannelInterceptorTests {
private static final String XOR_CSRF_TOKEN_VALUE = "wpe7zB62-NCpcA==";
private static final String INVALID_XOR_CSRF_TOKEN_VALUE = "KneoaygbRZtfHQ==";
private CsrfToken token;
private SimpMessageHeaderAccessor messageHeaders;
private MessageChannel channel;
private XorCsrfChannelInterceptor interceptor;
@BeforeEach
public void setup() {
this.token = new DefaultCsrfToken("header", "param", "token");
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
this.messageHeaders.setSessionAttributes(new HashMap<>());
this.channel = mock(MessageChannel.class);
this.interceptor = new XorCsrfChannelInterceptor();
}
@Test
public void preSendWhenConnectWithValidTokenThenSuccess() {
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenConnectWithInvalidTokenThenThrowsInvalidCsrfTokenException() {
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), INVALID_XOR_CSRF_TOKEN_VALUE);
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
@Test
public void preSendWhenConnectWithNoTokenThenThrowsInvalidCsrfTokenException() {
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
@Test
public void preSendWhenConnectWithMissingTokenThenThrowsMissingCsrfTokenException() {
// @formatter:off
assertThatExceptionOfType(MissingCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
@Test
public void preSendWhenConnectWithNullSessionAttributesThenThrowsMissingCsrfTokenException() {
this.messageHeaders.setSessionAttributes(null);
// @formatter:off
assertThatExceptionOfType(MissingCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
@Test
public void preSendWhenAckThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenDisconnectThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.DISCONNECT);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenHeartbeatThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.HEARTBEAT);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenMessageThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenOtherThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.OTHER);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenUnsubscribeThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.UNSUBSCRIBE);
this.interceptor.preSend(message(), this.channel);
}
private Message<String> message() {
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2023 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.
@@ -31,6 +31,7 @@ import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.web.socket.WebSocketHandler;
import static org.assertj.core.api.Assertions.assertThat;
@@ -72,10 +73,38 @@ public class CsrfTokenHandshakeInterceptorTests {
@Test
public void beforeHandshake() throws Exception {
CsrfToken token = new DefaultCsrfToken("header", "param", "token");
this.httpRequest.setAttribute(CsrfToken.class.getName(), token);
this.httpRequest.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(token));
this.interceptor.beforeHandshake(this.request, this.response, this.wsHandler, this.attributes);
assertThat(this.attributes.keySet()).containsOnly(CsrfToken.class.getName());
assertThat(this.attributes.values()).containsOnly(token);
CsrfToken csrfToken = (CsrfToken) this.attributes.get(CsrfToken.class.getName());
assertThat(csrfToken.getHeaderName()).isEqualTo(token.getHeaderName());
assertThat(csrfToken.getParameterName()).isEqualTo(token.getParameterName());
assertThat(csrfToken.getToken()).isEqualTo(token.getToken());
// Ensure the values of the CsrfToken are copied into a new token so the old token
// is available for garbage collection.
// This is required because the original token could hold a reference to the
// HttpServletRequest/Response of the handshake request.
assertThat(csrfToken).isNotSameAs(token);
}
private static final class TestDeferredCsrfToken implements DeferredCsrfToken {
private final CsrfToken csrfToken;
private TestDeferredCsrfToken(CsrfToken csrfToken) {
this.csrfToken = csrfToken;
}
@Override
public CsrfToken get() {
return this.csrfToken;
}
@Override
public boolean isGenerated() {
return false;
}
}
}