SEC-2830: Provide Same Origin support for SockJS

This commit is contained in:
Rob Winch
2015-02-18 09:14:10 -06:00
parent a27c33754c
commit 6a8475adbb
13 changed files with 872 additions and 6 deletions

View File

@@ -15,9 +15,14 @@
*/
package org.springframework.security.config.annotation.web.configurers;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.access.AccessDeniedHandler;
@@ -31,6 +36,9 @@ import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
@@ -66,7 +74,8 @@ import org.springframework.util.Assert;
*/
public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<CsrfConfigurer<H>,H> {
private CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();
private RequestMatcher requireCsrfProtectionMatcher;
private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;
private List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<RequestMatcher>();
/**
* Creates a new instance
@@ -102,10 +111,38 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>> extends Abst
return this;
}
/**
* <p>
* Allows specifying {@link HttpServletRequest} that should not use CSRF Protection even if they match the {@link #requireCsrfProtectionMatcher(RequestMatcher)}.
* </p>
*
* <p>
* The following will ensure CSRF protection ignores:
* </p>
* <ul>
* <li>Any GET, HEAD, TRACE, OPTIONS (this is the default)</li>
* <li>We also explicitly state to ignore any request that starts with "/sockjs/"</li>
* </ul>
*
* <pre>
* http
* .csrf()
* .ignoringAntMatchers("/sockjs/**")
* .and()
* ...
* </pre>
*
* @since 4.0
*/
public CsrfConfigurer<H> ignoringAntMatchers(String... antPatterns) {
return new IgnoreCsrfProtectionRegistry().antMatchers(antPatterns).and();
}
@SuppressWarnings("unchecked")
@Override
public void configure(H http) throws Exception {
CsrfFilter filter = new CsrfFilter(csrfTokenRepository);
RequestMatcher requireCsrfProtectionMatcher = getRequireCsrfProtectionMatcher();
if(requireCsrfProtectionMatcher != null) {
filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
}
@@ -125,6 +162,18 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>> extends Abst
http.addFilter(filter);
}
/**
* Gets the final {@link RequestMatcher} to use by combining the {@link #requireCsrfProtectionMatcher(RequestMatcher)} and any {@link #ignore()}.
*
* @return the {@link RequestMatcher} to use
*/
private RequestMatcher getRequireCsrfProtectionMatcher() {
if(ignoredCsrfProtectionMatchers.isEmpty()) {
return requireCsrfProtectionMatcher;
}
return new AndRequestMatcher(requireCsrfProtectionMatcher, new NegatedRequestMatcher(new OrRequestMatcher(ignoredCsrfProtectionMatchers)));
}
/**
* Gets the default {@link AccessDeniedHandler} from the
* {@link ExceptionHandlingConfigurer#getAccessDeniedHandler()} or create a
@@ -190,4 +239,25 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>> extends Abst
handlers.put(MissingCsrfTokenException.class, invalidSessionDeniedHandler);
return new DelegatingAccessDeniedHandler(handlers, defaultAccessDeniedHandler);
}
/**
* Allows registering {@link RequestMatcher} instances that should be
* ignored (even if the {@link HttpServletRequest} matches the
* {@link CsrfConfigurer#requireCsrfProtectionMatcher(RequestMatcher)}.
*
* @author Rob Winch
* @since 4.0
*/
private class IgnoreCsrfProtectionRegistry extends AbstractRequestMatcherRegistry<IgnoreCsrfProtectionRegistry>{
public CsrfConfigurer<H> and() {
return CsrfConfigurer.this;
}
protected IgnoreCsrfProtectionRegistry chainRequestMatchers(
List<RequestMatcher> requestMatchers) {
ignoredCsrfProtectionMatchers.addAll(requestMatchers);
return this;
}
}
}

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.security.config.annotation.web.socket;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
@@ -28,11 +31,20 @@ import org.springframework.security.messaging.access.intercept.ChannelSecurityIn
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsService;
import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler;
import org.springframework.web.socket.sockjs.transport.TransportHandlingSockJsService;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Allows configuring WebSocket Authorization.
@@ -58,9 +70,12 @@ import java.util.List;
* @author Rob Winch
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 100)
public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer {
public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer
implements SmartInitializingSingleton {
private final WebSocketMessageSecurityMetadataSourceRegistry inboundRegistry = new WebSocketMessageSecurityMetadataSourceRegistry();
private ApplicationContext context;
public void registerStompEndpoints(StompEndpointRegistry registry) {}
@Override
@@ -69,16 +84,34 @@ public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends A
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}
@Override
public final void configureClientInboundChannel(ChannelRegistration registration) {
ChannelSecurityInterceptor inboundChannelSecurity = inboundChannelSecurity();
registration.setInterceptors(securityContextChannelInterceptor());
if(sameOriginEnforced()) {
registration.setInterceptors(csrfChannelInterceptor());
}
if(inboundRegistry.containsMapping()) {
registration.setInterceptors(securityContextChannelInterceptor(),inboundChannelSecurity);
registration.setInterceptors(inboundChannelSecurity);
}
customizeClientInboundChannel(registration);
}
/**
* <p>
* Determines if a CSRF token is required for connecting. This protects against remote sites from connecting to the
* application and being able to read/write data over the connection. The default is true.
* </p>
* <p>
* Subclasses can override this method to disable CSRF protection
* </p>
*
* @return true if a CSRF is required for connecting, else false
*/
protected boolean sameOriginEnforced() {
return true;
}
/**
* Allows subclasses to customize the configuration of the {@link ChannelRegistration}.
*
@@ -87,6 +120,11 @@ public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends A
protected void customizeClientInboundChannel(ChannelRegistration registration) {
}
@Bean
public CsrfChannelInterceptor csrfChannelInterceptor() {
return new CsrfChannelInterceptor();
}
@Bean
public ChannelSecurityInterceptor inboundChannelSecurity() {
ChannelSecurityInterceptor channelSecurityInterceptor = new ChannelSecurityInterceptor(inboundMessageSecurityMetadataSource());
@@ -125,4 +163,47 @@ public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends A
return super.containsMapping();
}
}
@Autowired
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
public void afterSingletonsInstantiated() {
if(!sameOriginEnforced()) {
return;
}
String beanName = "stompWebSocketHandlerMapping";
SimpleUrlHandlerMapping mapping = context.getBean(beanName, SimpleUrlHandlerMapping.class);
Map<String, Object> mappings = mapping.getHandlerMap();
for(Object object : mappings.values()) {
if(object instanceof SockJsHttpRequestHandler) {
SockJsHttpRequestHandler sockjsHandler = (SockJsHttpRequestHandler) object;
SockJsService sockJsService = sockjsHandler.getSockJsService();
if(!(sockJsService instanceof TransportHandlingSockJsService)) {
throw new IllegalStateException("sockJsService must be instance of TransportHandlingSockJsService got " + sockJsService);
}
TransportHandlingSockJsService transportHandlingSockJsService = (TransportHandlingSockJsService) sockJsService;
List<HandshakeInterceptor> handshakeInterceptors = transportHandlingSockJsService.getHandshakeInterceptors();
List<HandshakeInterceptor> interceptorsToSet = new ArrayList<HandshakeInterceptor>(handshakeInterceptors.size() + 1);
interceptorsToSet.add(new CsrfTokenHandshakeInterceptor());
interceptorsToSet.addAll(handshakeInterceptors);
transportHandlingSockJsService.setHandshakeInterceptors(interceptorsToSet);
}
else if(object instanceof WebSocketHttpRequestHandler) {
WebSocketHttpRequestHandler handler = (WebSocketHttpRequestHandler) object;
List<HandshakeInterceptor> handshakeInterceptors = handler.getHandshakeInterceptors();
List<HandshakeInterceptor> interceptorsToSet = new ArrayList<HandshakeInterceptor>(handshakeInterceptors.size() + 1);
interceptorsToSet.add(new CsrfTokenHandshakeInterceptor());
interceptorsToSet.addAll(handshakeInterceptors);
handler.setHandshakeInterceptors(interceptorsToSet);
} else {
throw new IllegalStateException("Bean " + beanName + " is expected to contain mappings to either a SockJsHttpRequestHandler or a WebSocketHttpRequestHandler but got " + object);
}
}
}
}

View File

@@ -33,6 +33,8 @@ import org.springframework.security.messaging.access.intercept.ChannelSecurityIn
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
@@ -152,7 +154,8 @@ public final class MessageSecurityBeanDefinitionParser implements BeanDefinition
String[] beanNames = registry.getBeanDefinitionNames();
for(String beanName : beanNames) {
BeanDefinition bd = registry.getBeanDefinition(beanName);
if(bd.getBeanClassName().equals(SimpAnnotationMethodMessageHandler.class.getName())) {
String beanClassName = bd.getBeanClassName();
if(beanClassName.equals(SimpAnnotationMethodMessageHandler.class.getName())) {
PropertyValue current = bd.getPropertyValues().getPropertyValue(CUSTOM_ARG_RESOLVERS_PROP);
ManagedList<Object> argResolvers = new ManagedList<Object>();
if(current != null) {
@@ -161,6 +164,13 @@ public final class MessageSecurityBeanDefinitionParser implements BeanDefinition
argResolvers.add(new RootBeanDefinition(AuthenticationPrincipalArgumentResolver.class));
bd.getPropertyValues().add(CUSTOM_ARG_RESOLVERS_PROP, argResolvers);
}
else if(beanClassName.equals("org.springframework.web.socket.server.support.WebSocketHttpRequestHandler")) {
addCsrfTokenHandshakeInterceptor(bd);
} else if(beanClassName.equals("org.springframework.web.socket.sockjs.transport.TransportHandlingSockJsService")) {
addCsrfTokenHandshakeInterceptor(bd);
} else if(beanClassName.equals("org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService")) {
addCsrfTokenHandshakeInterceptor(bd);
}
}
if(!registry.containsBeanDefinition(CLIENT_INBOUND_CHANNEL_BEAN_ID)) {
@@ -168,6 +178,7 @@ public final class MessageSecurityBeanDefinitionParser implements BeanDefinition
}
ManagedList<Object> interceptors = new ManagedList();
interceptors.add(new RootBeanDefinition(SecurityContextChannelInterceptor.class));
interceptors.add(new RootBeanDefinition(CsrfChannelInterceptor.class));
interceptors.add(registry.getBeanDefinition(inboundSecurityInterceptorId));
BeanDefinition inboundChannel = registry.getBeanDefinition(CLIENT_INBOUND_CHANNEL_BEAN_ID);
@@ -180,6 +191,14 @@ public final class MessageSecurityBeanDefinitionParser implements BeanDefinition
inboundChannel.getPropertyValues().add(INTERCEPTORS_PROP, interceptors);
}
private void addCsrfTokenHandshakeInterceptor(BeanDefinition bd) {
String interceptorPropertyName = "handshakeInterceptors";
ManagedList<? super Object> interceptors = new ManagedList<Object>();
interceptors.add(new RootBeanDefinition(CsrfTokenHandshakeInterceptor.class));
interceptors.addAll((ManagedList<Object>)bd.getPropertyValues().get(interceptorPropertyName));
bd.getPropertyValues().add(interceptorPropertyName, interceptors);
}
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}