Add XorCsrfChannelInterceptor
Issue gh-12378
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user