VYPR
Moderate severityNVD Advisory· Published May 11, 2018· Updated Sep 16, 2024

CVE-2018-1257

CVE-2018-1257

Description

Spring Framework, versions 5.0.x prior to 5.0.6, versions 4.3.x prior to 4.3.17, and older unsupported versions allows applications to expose STOMP over WebSocket endpoints with a simple, in-memory STOMP broker through the spring-messaging module. A malicious user (or attacker) can craft a message to the broker that can lead to a regular expression, denial of service attack.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Spring Framework STOMP over WebSocket broker vulnerable to ReDoS via crafted selector header, causing denial of service.

Vulnerability

Spring Framework versions 5.0.x prior to 5.0.6, 4.3.x prior to 4.3.17, and older unsupported versions expose a STOMP over WebSocket endpoint using a simple in-memory broker via the spring-messaging module. The DefaultSubscriptionRegistry in the broker processes selector headers using a regular expression that can be exploited to cause catastrophic backtracking, leading to a denial of service condition [1][2].

Exploitation

An attacker must be able to send STOMP messages to the WebSocket endpoint. If the endpoint is unauthenticated, the attacker can directly send a subscription frame with a specially crafted selector header containing a malicious payload designed to trigger exponential backtracking in the regex engine [1].

Impact

Successful exploitation results in a regular expression denial of service (ReDoS) attack, consuming excessive CPU resources and potentially making the application unresponsive. This can lead to a denial of service for legitimate users [1].

Mitigation

Upgrade to Spring Framework 5.0.6, 4.3.17, or later. As a workaround, the selector header support can be disabled by setting the selectorHeaderName property to null via configuration. The fix is included in commits that allow the header name to be null [3][4]. No CISA KEV listing or EOL status has been published for this CVE.

AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.springframework:spring-coreMaven
>= 5.0.0, < 5.0.65.0.6
org.springframework:spring-coreMaven
< 4.3.174.3.17

Affected products

2

Patches

4
246a6db1cad2

Selector header name is exposed for configuration

https://github.com/spring-projects/spring-frameworkRossen StoyanchevApr 17, 2018via ghsa
8 files changed · +144 34
  • spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+32 24 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -43,6 +43,7 @@
     import org.springframework.util.LinkedMultiValueMap;
     import org.springframework.util.MultiValueMap;
     import org.springframework.util.PathMatcher;
    +import org.springframework.util.StringUtils;
     
     /**
      * Implementation of {@link SubscriptionRegistry} that stores subscriptions
    @@ -113,24 +114,25 @@ public int getCacheLimit() {
     	}
     
     	/**
    -	 * Configure the name of a selector header that a subscription message can
    -	 * have in order to filter messages based on their headers. The value of the
    -	 * header can use Spring EL expressions against message headers.
    -	 * <p>For example the following expression expects a header called "foo" to
    -	 * have the value "bar":
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
     	 * <pre>
     	 * headers.foo == 'bar'
     	 * </pre>
    -	 * <p>By default this is set to "selector".
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
     	 * @since 4.2
     	 */
     	public void setSelectorHeaderName(String selectorHeaderName) {
    -		Assert.notNull(selectorHeaderName, "'selectorHeaderName' must not be null");
    -		this.selectorHeaderName = selectorHeaderName;
    +		this.selectorHeaderName = StringUtils.hasText(selectorHeaderName) ? selectorHeaderName : null;
     	}
     
     	/**
    -	 * Return the name for the selector header.
    +	 * Return the name for the selector header name.
     	 * @since 4.2
     	 */
     	public String getSelectorHeaderName() {
    @@ -142,25 +144,31 @@ public String getSelectorHeaderName() {
     	protected void addSubscriptionInternal(
     			String sessionId, String subsId, String destination, Message<?> message) {
     
    +		Expression expression = getSelectorExpression(message.getHeaders());
    +		this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
    +		this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    +	}
    +
    +	private Expression getSelectorExpression(MessageHeaders headers) {
     		Expression expression = null;
    -		MessageHeaders headers = message.getHeaders();
    -		String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
    -		if (selector != null) {
    -			try {
    -				expression = this.expressionParser.parseExpression(selector);
    -				this.selectorHeaderInUse = true;
    -				if (logger.isTraceEnabled()) {
    -					logger.trace("Subscription selector: [" + selector + "]");
    +		if (getSelectorHeaderName() != null) {
    +			String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
    +			if (selector != null) {
    +				try {
    +					expression = this.expressionParser.parseExpression(selector);
    +					this.selectorHeaderInUse = true;
    +					if (logger.isTraceEnabled()) {
    +						logger.trace("Subscription selector: [" + selector + "]");
    +					}
     				}
    -			}
    -			catch (Throwable ex) {
    -				if (logger.isDebugEnabled()) {
    -					logger.debug("Failed to parse selector: " + selector, ex);
    +				catch (Throwable ex) {
    +					if (logger.isDebugEnabled()) {
    +						logger.debug("Failed to parse selector: " + selector, ex);
    +					}
     				}
     			}
     		}
    -		this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
    -		this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    +		return expression;
     	}
     
     	@Override
    
  • spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java+38 6 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -51,23 +51,27 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
     
     	private static final byte[] EMPTY_PAYLOAD = new byte[0];
     
    -	private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<String, SessionInfo>();
    -
    -	private SubscriptionRegistry subscriptionRegistry;
     
     	private PathMatcher pathMatcher;
     
     	private Integer cacheLimit;
     
    +	private String selectorHeaderName = "selector";
    +
     	private TaskScheduler taskScheduler;
     
     	private long[] heartbeatValue;
     
    -	private ScheduledFuture<?> heartbeatFuture;
    -
     	private MessageHeaderInitializer headerInitializer;
     
     
    +	private SubscriptionRegistry subscriptionRegistry;
    +
    +	private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<String, SessionInfo>();
    +
    +	private ScheduledFuture<?> heartbeatFuture;
    +
    +
     	/**
     	 * Create a SimpleBrokerMessageHandler instance with the given message channels
     	 * and destination prefixes.
    @@ -96,12 +100,40 @@ public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) {
     		this.subscriptionRegistry = subscriptionRegistry;
     		initPathMatcherToUse();
     		initCacheLimitToUse();
    +		initSelectorHeaderNameToUse();
     	}
     
     	public SubscriptionRegistry getSubscriptionRegistry() {
     		return this.subscriptionRegistry;
     	}
     
    +	/**
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
    +	 * <pre>
    +	 * headers.foo == 'bar'
    +	 * </pre>
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
    +	 * @since 4.3.17
    +	 * @see #setSubscriptionRegistry
    +	 * @see DefaultSubscriptionRegistry#setSelectorHeaderName(String)
    +	 */
    +	public void setSelectorHeaderName(String selectorHeaderName) {
    +		this.selectorHeaderName = selectorHeaderName;
    +		initSelectorHeaderNameToUse();
    +	}
    +
    +	private void initSelectorHeaderNameToUse() {
    +		if (this.subscriptionRegistry instanceof DefaultSubscriptionRegistry) {
    +			((DefaultSubscriptionRegistry) this.subscriptionRegistry).setSelectorHeaderName(this.selectorHeaderName);
    +		}
    +	}
    +
     	/**
     	 * When configured, the given PathMatcher is passed down to the underlying
     	 * SubscriptionRegistry to use for matching destination to subscriptions.
    
  • spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java+22 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2014 the original author or authors.
    + * 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.
    @@ -33,6 +33,8 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
     
     	private long[] heartbeat;
     
    +	private String selectorHeaderName = "selector";
    +
     
     	public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
     		super(inChannel, outChannel, prefixes);
    @@ -65,6 +67,24 @@ public SimpleBrokerRegistration setHeartbeatValue(long[] heartbeat) {
     		return this;
     	}
     
    +	/**
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
    +	 * <pre>
    +	 * headers.foo == 'bar'
    +	 * </pre>
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
    +	 * @since 4.3.17
    +	 */
    +	public void setSelectorHeaderName(String selectorHeaderName) {
    +		this.selectorHeaderName = selectorHeaderName;
    +	}
    +
     
     	@Override
     	protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
    @@ -76,6 +96,7 @@ protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel broke
     		if (this.heartbeat != null) {
     			handler.setHeartbeatValue(this.heartbeat);
     		}
    +		handler.setSelectorHeaderName(this.selectorHeaderName);
     		return handler;
     	}
     
    
  • spring-messaging/src/test/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistryTests.java+25 0 modified
    @@ -264,6 +264,8 @@ public void registerSubscriptionWithSelector() throws Exception {
     		String destination = "/foo";
     		String selector = "headers.foo == 'bar'";
     
    +		// First, try with selector header
    +
     		this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
     
     		SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
    @@ -276,11 +278,34 @@ public void registerSubscriptionWithSelector() throws Exception {
     		assertEquals(1, actual.size());
     		assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
     
    +		// Then without
    +
     		actual = this.registry.findSubscriptions(createMessage(destination));
     		assertNotNull(actual);
     		assertEquals(0, actual.size());
     	}
     
    +	@Test
    +	public void registerSubscriptionWithSelectorNotSupported() {
    +		String sessionId = "sess01";
    +		String subscriptionId = "subs01";
    +		String destination = "/foo";
    +		String selector = "headers.foo == 'bar'";
    +
    +		this.registry.setSelectorHeaderName(null);
    +		this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
    +
    +		SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
    +		accessor.setDestination(destination);
    +		accessor.setNativeHeader("foo", "bazz");
    +		Message<?> message = MessageBuilder.createMessage("", accessor.getMessageHeaders());
    +
    +		MultiValueMap<String, String> actual = this.registry.findSubscriptions(message);
    +		assertNotNull(actual);
    +		assertEquals(1, actual.size());
    +		assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
    +	}
    +
     	@Test  // SPR-11931
     	public void registerSubscriptionTwiceAndUnregister() {
     		this.registry.registerSubscription(subscribeMessage("sess01", "subs01", "/foo"));
    
  • spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java+5 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -381,6 +381,10 @@ private RootBeanDefinition registerMessageBroker(Element brokerElement,
     				String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
     				brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
     			}
    +			if (simpleBrokerElem.hasAttribute("selector-header")) {
    +				String headerName = simpleBrokerElem.getAttribute("selector-header");
    +				brokerDef.getPropertyValues().add("selectorHeaderName", headerName);
    +			}
     		}
     		else if (brokerRelayElem != null) {
     			String prefix = brokerRelayElem.getAttribute("prefix");
    
  • spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.3.xsd+16 0 modified
    @@ -384,6 +384,22 @@
                     ]]></xsd:documentation>
     			</xsd:annotation>
     		</xsd:attribute>
    +		<xsd:attribute name="selector-header" type="xsd:string">
    +			<xsd:annotation>
    +				<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
    +	Configure the name of a header that a subscription message can have for
    +	the purpose of filtering messages matched to the subscription. The header
    +	value is expected to be a Spring EL boolean expression to be applied to
    +	the headers of messages matched to the subscription.
    +
    +	For example:
    +	headers.foo == 'bar'
    +
    +	By default this is set to "selector". You can set it to a different
    +	name, or to "" to turn off support for a selector header.
    +                ]]></xsd:documentation>
    +			</xsd:annotation>
    +		</xsd:attribute>
     	</xsd:complexType>
     
     	<xsd:complexType name="channel">
    
  • spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java+4 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -45,6 +45,7 @@
     import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
     import org.springframework.messaging.simp.SimpMessagingTemplate;
     import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
    +import org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry;
     import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
     import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
     import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
    @@ -199,6 +200,8 @@ public void simpleBroker() throws Exception {
     		assertNotNull(brokerMessageHandler);
     		Collection<String> prefixes = brokerMessageHandler.getDestinationPrefixes();
     		assertEquals(Arrays.asList("/topic", "/queue"), new ArrayList<String>(prefixes));
    +		DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) brokerMessageHandler.getSubscriptionRegistry();
    +		assertEquals("my-selector", registry.getSelectorHeaderName());
     		assertNotNull(brokerMessageHandler.getTaskScheduler());
     		assertArrayEquals(new long[] {15000, 15000}, brokerMessageHandler.getHeartbeatValue());
     
    
  • spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml+2 1 modified
    @@ -35,7 +35,8 @@
     
     		<websocket:stomp-error-handler ref="errorHandler" />
     
    -		<websocket:simple-broker prefix="/topic, /queue" heartbeat="15000,15000" scheduler="scheduler" />
    +		<websocket:simple-broker prefix="/topic, /queue" selector-header="my-selector"
    +				heartbeat="15000,15000" scheduler="scheduler" />
     
     	</websocket:message-broker>
     
    
246a6db1cad2

Selector header name is exposed for configuration

https://github.com/spring-projects/spring-frameworkRossen StoyanchevApr 17, 2018via ghsa
8 files changed · +144 34
  • spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+32 24 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -43,6 +43,7 @@
     import org.springframework.util.LinkedMultiValueMap;
     import org.springframework.util.MultiValueMap;
     import org.springframework.util.PathMatcher;
    +import org.springframework.util.StringUtils;
     
     /**
      * Implementation of {@link SubscriptionRegistry} that stores subscriptions
    @@ -113,24 +114,25 @@ public int getCacheLimit() {
     	}
     
     	/**
    -	 * Configure the name of a selector header that a subscription message can
    -	 * have in order to filter messages based on their headers. The value of the
    -	 * header can use Spring EL expressions against message headers.
    -	 * <p>For example the following expression expects a header called "foo" to
    -	 * have the value "bar":
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
     	 * <pre>
     	 * headers.foo == 'bar'
     	 * </pre>
    -	 * <p>By default this is set to "selector".
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
     	 * @since 4.2
     	 */
     	public void setSelectorHeaderName(String selectorHeaderName) {
    -		Assert.notNull(selectorHeaderName, "'selectorHeaderName' must not be null");
    -		this.selectorHeaderName = selectorHeaderName;
    +		this.selectorHeaderName = StringUtils.hasText(selectorHeaderName) ? selectorHeaderName : null;
     	}
     
     	/**
    -	 * Return the name for the selector header.
    +	 * Return the name for the selector header name.
     	 * @since 4.2
     	 */
     	public String getSelectorHeaderName() {
    @@ -142,25 +144,31 @@ public String getSelectorHeaderName() {
     	protected void addSubscriptionInternal(
     			String sessionId, String subsId, String destination, Message<?> message) {
     
    +		Expression expression = getSelectorExpression(message.getHeaders());
    +		this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
    +		this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    +	}
    +
    +	private Expression getSelectorExpression(MessageHeaders headers) {
     		Expression expression = null;
    -		MessageHeaders headers = message.getHeaders();
    -		String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
    -		if (selector != null) {
    -			try {
    -				expression = this.expressionParser.parseExpression(selector);
    -				this.selectorHeaderInUse = true;
    -				if (logger.isTraceEnabled()) {
    -					logger.trace("Subscription selector: [" + selector + "]");
    +		if (getSelectorHeaderName() != null) {
    +			String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
    +			if (selector != null) {
    +				try {
    +					expression = this.expressionParser.parseExpression(selector);
    +					this.selectorHeaderInUse = true;
    +					if (logger.isTraceEnabled()) {
    +						logger.trace("Subscription selector: [" + selector + "]");
    +					}
     				}
    -			}
    -			catch (Throwable ex) {
    -				if (logger.isDebugEnabled()) {
    -					logger.debug("Failed to parse selector: " + selector, ex);
    +				catch (Throwable ex) {
    +					if (logger.isDebugEnabled()) {
    +						logger.debug("Failed to parse selector: " + selector, ex);
    +					}
     				}
     			}
     		}
    -		this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
    -		this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    +		return expression;
     	}
     
     	@Override
    
  • spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java+38 6 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -51,23 +51,27 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
     
     	private static final byte[] EMPTY_PAYLOAD = new byte[0];
     
    -	private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<String, SessionInfo>();
    -
    -	private SubscriptionRegistry subscriptionRegistry;
     
     	private PathMatcher pathMatcher;
     
     	private Integer cacheLimit;
     
    +	private String selectorHeaderName = "selector";
    +
     	private TaskScheduler taskScheduler;
     
     	private long[] heartbeatValue;
     
    -	private ScheduledFuture<?> heartbeatFuture;
    -
     	private MessageHeaderInitializer headerInitializer;
     
     
    +	private SubscriptionRegistry subscriptionRegistry;
    +
    +	private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<String, SessionInfo>();
    +
    +	private ScheduledFuture<?> heartbeatFuture;
    +
    +
     	/**
     	 * Create a SimpleBrokerMessageHandler instance with the given message channels
     	 * and destination prefixes.
    @@ -96,12 +100,40 @@ public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) {
     		this.subscriptionRegistry = subscriptionRegistry;
     		initPathMatcherToUse();
     		initCacheLimitToUse();
    +		initSelectorHeaderNameToUse();
     	}
     
     	public SubscriptionRegistry getSubscriptionRegistry() {
     		return this.subscriptionRegistry;
     	}
     
    +	/**
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
    +	 * <pre>
    +	 * headers.foo == 'bar'
    +	 * </pre>
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
    +	 * @since 4.3.17
    +	 * @see #setSubscriptionRegistry
    +	 * @see DefaultSubscriptionRegistry#setSelectorHeaderName(String)
    +	 */
    +	public void setSelectorHeaderName(String selectorHeaderName) {
    +		this.selectorHeaderName = selectorHeaderName;
    +		initSelectorHeaderNameToUse();
    +	}
    +
    +	private void initSelectorHeaderNameToUse() {
    +		if (this.subscriptionRegistry instanceof DefaultSubscriptionRegistry) {
    +			((DefaultSubscriptionRegistry) this.subscriptionRegistry).setSelectorHeaderName(this.selectorHeaderName);
    +		}
    +	}
    +
     	/**
     	 * When configured, the given PathMatcher is passed down to the underlying
     	 * SubscriptionRegistry to use for matching destination to subscriptions.
    
  • spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java+22 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2014 the original author or authors.
    + * 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.
    @@ -33,6 +33,8 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
     
     	private long[] heartbeat;
     
    +	private String selectorHeaderName = "selector";
    +
     
     	public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
     		super(inChannel, outChannel, prefixes);
    @@ -65,6 +67,24 @@ public SimpleBrokerRegistration setHeartbeatValue(long[] heartbeat) {
     		return this;
     	}
     
    +	/**
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
    +	 * <pre>
    +	 * headers.foo == 'bar'
    +	 * </pre>
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
    +	 * @since 4.3.17
    +	 */
    +	public void setSelectorHeaderName(String selectorHeaderName) {
    +		this.selectorHeaderName = selectorHeaderName;
    +	}
    +
     
     	@Override
     	protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
    @@ -76,6 +96,7 @@ protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel broke
     		if (this.heartbeat != null) {
     			handler.setHeartbeatValue(this.heartbeat);
     		}
    +		handler.setSelectorHeaderName(this.selectorHeaderName);
     		return handler;
     	}
     
    
  • spring-messaging/src/test/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistryTests.java+25 0 modified
    @@ -264,6 +264,8 @@ public void registerSubscriptionWithSelector() throws Exception {
     		String destination = "/foo";
     		String selector = "headers.foo == 'bar'";
     
    +		// First, try with selector header
    +
     		this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
     
     		SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
    @@ -276,11 +278,34 @@ public void registerSubscriptionWithSelector() throws Exception {
     		assertEquals(1, actual.size());
     		assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
     
    +		// Then without
    +
     		actual = this.registry.findSubscriptions(createMessage(destination));
     		assertNotNull(actual);
     		assertEquals(0, actual.size());
     	}
     
    +	@Test
    +	public void registerSubscriptionWithSelectorNotSupported() {
    +		String sessionId = "sess01";
    +		String subscriptionId = "subs01";
    +		String destination = "/foo";
    +		String selector = "headers.foo == 'bar'";
    +
    +		this.registry.setSelectorHeaderName(null);
    +		this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
    +
    +		SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
    +		accessor.setDestination(destination);
    +		accessor.setNativeHeader("foo", "bazz");
    +		Message<?> message = MessageBuilder.createMessage("", accessor.getMessageHeaders());
    +
    +		MultiValueMap<String, String> actual = this.registry.findSubscriptions(message);
    +		assertNotNull(actual);
    +		assertEquals(1, actual.size());
    +		assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
    +	}
    +
     	@Test  // SPR-11931
     	public void registerSubscriptionTwiceAndUnregister() {
     		this.registry.registerSubscription(subscribeMessage("sess01", "subs01", "/foo"));
    
  • spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java+5 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -381,6 +381,10 @@ private RootBeanDefinition registerMessageBroker(Element brokerElement,
     				String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
     				brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
     			}
    +			if (simpleBrokerElem.hasAttribute("selector-header")) {
    +				String headerName = simpleBrokerElem.getAttribute("selector-header");
    +				brokerDef.getPropertyValues().add("selectorHeaderName", headerName);
    +			}
     		}
     		else if (brokerRelayElem != null) {
     			String prefix = brokerRelayElem.getAttribute("prefix");
    
  • spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.3.xsd+16 0 modified
    @@ -384,6 +384,22 @@
                     ]]></xsd:documentation>
     			</xsd:annotation>
     		</xsd:attribute>
    +		<xsd:attribute name="selector-header" type="xsd:string">
    +			<xsd:annotation>
    +				<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
    +	Configure the name of a header that a subscription message can have for
    +	the purpose of filtering messages matched to the subscription. The header
    +	value is expected to be a Spring EL boolean expression to be applied to
    +	the headers of messages matched to the subscription.
    +
    +	For example:
    +	headers.foo == 'bar'
    +
    +	By default this is set to "selector". You can set it to a different
    +	name, or to "" to turn off support for a selector header.
    +                ]]></xsd:documentation>
    +			</xsd:annotation>
    +		</xsd:attribute>
     	</xsd:complexType>
     
     	<xsd:complexType name="channel">
    
  • spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java+4 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -45,6 +45,7 @@
     import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
     import org.springframework.messaging.simp.SimpMessagingTemplate;
     import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
    +import org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry;
     import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
     import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
     import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
    @@ -199,6 +200,8 @@ public void simpleBroker() throws Exception {
     		assertNotNull(brokerMessageHandler);
     		Collection<String> prefixes = brokerMessageHandler.getDestinationPrefixes();
     		assertEquals(Arrays.asList("/topic", "/queue"), new ArrayList<String>(prefixes));
    +		DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) brokerMessageHandler.getSubscriptionRegistry();
    +		assertEquals("my-selector", registry.getSelectorHeaderName());
     		assertNotNull(brokerMessageHandler.getTaskScheduler());
     		assertArrayEquals(new long[] {15000, 15000}, brokerMessageHandler.getHeartbeatValue());
     
    
  • spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml+2 1 modified
    @@ -35,7 +35,8 @@
     
     		<websocket:stomp-error-handler ref="errorHandler" />
     
    -		<websocket:simple-broker prefix="/topic, /queue" heartbeat="15000,15000" scheduler="scheduler" />
    +		<websocket:simple-broker prefix="/topic, /queue" selector-header="my-selector"
    +				heartbeat="15000,15000" scheduler="scheduler" />
     
     	</websocket:message-broker>
     
    
ff2228fdaf13

Selector header name is exposed for configuration

https://github.com/spring-projects/spring-frameworkRossen StoyanchevApr 17, 2018via ghsa
8 files changed · +150 34
  • spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+35 24 modified
    @@ -44,6 +44,7 @@
     import org.springframework.util.LinkedMultiValueMap;
     import org.springframework.util.MultiValueMap;
     import org.springframework.util.PathMatcher;
    +import org.springframework.util.StringUtils;
     
     /**
      * Implementation of {@link SubscriptionRegistry} that stores subscriptions
    @@ -73,6 +74,7 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
     
     	private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
     
    +	@Nullable
     	private String selectorHeaderName = "selector";
     
     	private volatile boolean selectorHeaderInUse = false;
    @@ -114,26 +116,28 @@ public int getCacheLimit() {
     	}
     
     	/**
    -	 * Configure the name of a selector header that a subscription message can
    -	 * have in order to filter messages based on their headers. The value of the
    -	 * header can use Spring EL expressions against message headers.
    -	 * <p>For example the following expression expects a header called "foo" to
    -	 * have the value "bar":
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
     	 * <pre>
     	 * headers.foo == 'bar'
     	 * </pre>
    -	 * <p>By default this is set to "selector".
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
     	 * @since 4.2
     	 */
    -	public void setSelectorHeaderName(String selectorHeaderName) {
    -		Assert.notNull(selectorHeaderName, "'selectorHeaderName' must not be null");
    -		this.selectorHeaderName = selectorHeaderName;
    +	public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
    +		this.selectorHeaderName = StringUtils.hasText(selectorHeaderName) ? selectorHeaderName : null;
     	}
     
     	/**
    -	 * Return the name for the selector header.
    +	 * Return the name for the selector header name.
     	 * @since 4.2
     	 */
    +	@Nullable
     	public String getSelectorHeaderName() {
     		return this.selectorHeaderName;
     	}
    @@ -143,25 +147,32 @@ public String getSelectorHeaderName() {
     	protected void addSubscriptionInternal(
     			String sessionId, String subsId, String destination, Message<?> message) {
     
    +		Expression expression = getSelectorExpression(message.getHeaders());
    +		this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
    +		this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    +	}
    +
    +	@Nullable
    +	private Expression getSelectorExpression(MessageHeaders headers) {
     		Expression expression = null;
    -		MessageHeaders headers = message.getHeaders();
    -		String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
    -		if (selector != null) {
    -			try {
    -				expression = this.expressionParser.parseExpression(selector);
    -				this.selectorHeaderInUse = true;
    -				if (logger.isTraceEnabled()) {
    -					logger.trace("Subscription selector: [" + selector + "]");
    +		if (getSelectorHeaderName() != null) {
    +			String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
    +			if (selector != null) {
    +				try {
    +					expression = this.expressionParser.parseExpression(selector);
    +					this.selectorHeaderInUse = true;
    +					if (logger.isTraceEnabled()) {
    +						logger.trace("Subscription selector: [" + selector + "]");
    +					}
     				}
    -			}
    -			catch (Throwable ex) {
    -				if (logger.isDebugEnabled()) {
    -					logger.debug("Failed to parse selector: " + selector, ex);
    +				catch (Throwable ex) {
    +					if (logger.isDebugEnabled()) {
    +						logger.debug("Failed to parse selector: " + selector, ex);
    +					}
     				}
     			}
     		}
    -		this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
    -		this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    +		return expression;
     	}
     
     	@Override
    
  • spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java+38 5 modified
    @@ -51,27 +51,32 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
     
     	private static final byte[] EMPTY_PAYLOAD = new byte[0];
     
    -	private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
    -
    -	private SubscriptionRegistry subscriptionRegistry;
     
     	@Nullable
     	private PathMatcher pathMatcher;
     
     	@Nullable
     	private Integer cacheLimit;
     
    +	@Nullable
    +	private String selectorHeaderName = "selector";
    +
     	@Nullable
     	private TaskScheduler taskScheduler;
     
     	@Nullable
     	private long[] heartbeatValue;
     
     	@Nullable
    -	private ScheduledFuture<?> heartbeatFuture;
    +	private MessageHeaderInitializer headerInitializer;
    +
    +
    +	private SubscriptionRegistry subscriptionRegistry;
    +
    +	private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
     
     	@Nullable
    -	private MessageHeaderInitializer headerInitializer;
    +	private ScheduledFuture<?> heartbeatFuture;
     
     
     	/**
    @@ -102,6 +107,7 @@ public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) {
     		this.subscriptionRegistry = subscriptionRegistry;
     		initPathMatcherToUse();
     		initCacheLimitToUse();
    +		initSelectorHeaderNameToUse();
     	}
     
     	public SubscriptionRegistry getSubscriptionRegistry() {
    @@ -149,6 +155,33 @@ private void initCacheLimitToUse() {
     		}
     	}
     
    +	/**
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
    +	 * <pre>
    +	 * headers.foo == 'bar'
    +	 * </pre>
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
    +	 * @since 4.3.17
    +	 * @see #setSubscriptionRegistry
    +	 * @see DefaultSubscriptionRegistry#setSelectorHeaderName(String)
    +	 */
    +	public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
    +		this.selectorHeaderName = selectorHeaderName;
    +		initSelectorHeaderNameToUse();
    +	}
    +
    +	private void initSelectorHeaderNameToUse() {
    +		if (this.subscriptionRegistry instanceof DefaultSubscriptionRegistry) {
    +			((DefaultSubscriptionRegistry) this.subscriptionRegistry).setSelectorHeaderName(this.selectorHeaderName);
    +		}
    +	}
    +
     	/**
     	 * Configure the {@link org.springframework.scheduling.TaskScheduler} to
     	 * use for providing heartbeat support. Setting this property also sets the
    
  • spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java+23 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -36,6 +36,9 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
     	@Nullable
     	private long[] heartbeat;
     
    +	@Nullable
    +	private String selectorHeaderName = "selector";
    +
     
     	public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
     		super(inChannel, outChannel, prefixes);
    @@ -68,6 +71,24 @@ public SimpleBrokerRegistration setHeartbeatValue(long[] heartbeat) {
     		return this;
     	}
     
    +	/**
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
    +	 * <pre>
    +	 * headers.foo == 'bar'
    +	 * </pre>
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
    +	 * @since 4.3.17
    +	 */
    +	public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
    +		this.selectorHeaderName = selectorHeaderName;
    +	}
    +
     
     	@Override
     	protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
    @@ -79,6 +100,7 @@ protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel broke
     		if (this.heartbeat != null) {
     			handler.setHeartbeatValue(this.heartbeat);
     		}
    +		handler.setSelectorHeaderName(this.selectorHeaderName);
     		return handler;
     	}
     
    
  • spring-messaging/src/test/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistryTests.java+26 1 modified
    @@ -258,14 +258,16 @@ public void registerSubscriptionWithDestinationPatternRegex() {
     	}
     
     	@Test
    -	public void registerSubscriptionWithSelector() throws Exception {
    +	public void registerSubscriptionWithSelector() {
     		String sessionId = "sess01";
     		String subscriptionId = "subs01";
     		String destination = "/foo";
     		String selector = "headers.foo == 'bar'";
     
     		this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
     
    +		// First, try with selector header
    +
     		SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
     		accessor.setDestination(destination);
     		accessor.setNativeHeader("foo", "bar");
    @@ -276,11 +278,34 @@ public void registerSubscriptionWithSelector() throws Exception {
     		assertEquals(1, actual.size());
     		assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
     
    +		// Then without
    +
     		actual = this.registry.findSubscriptions(createMessage(destination));
     		assertNotNull(actual);
     		assertEquals(0, actual.size());
     	}
     
    +	@Test
    +	public void registerSubscriptionWithSelectorNotSupported() {
    +		String sessionId = "sess01";
    +		String subscriptionId = "subs01";
    +		String destination = "/foo";
    +		String selector = "headers.foo == 'bar'";
    +
    +		this.registry.setSelectorHeaderName(null);
    +		this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
    +
    +		SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
    +		accessor.setDestination(destination);
    +		accessor.setNativeHeader("foo", "bazz");
    +		Message<?> message = MessageBuilder.createMessage("", accessor.getMessageHeaders());
    +
    +		MultiValueMap<String, String> actual = this.registry.findSubscriptions(message);
    +		assertNotNull(actual);
    +		assertEquals(1, actual.size());
    +		assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
    +	}
    +
     	@Test  // SPR-11931
     	public void registerSubscriptionTwiceAndUnregister() {
     		this.registry.registerSubscription(subscribeMessage("sess01", "subs01", "/foo"));
    
  • spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java+4 0 modified
    @@ -383,6 +383,10 @@ private RootBeanDefinition registerMessageBroker(Element brokerElement,
     				String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
     				brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
     			}
    +			if (simpleBrokerElem.hasAttribute("selector-header")) {
    +				String headerName = simpleBrokerElem.getAttribute("selector-header");
    +				brokerDef.getPropertyValues().add("selectorHeaderName", headerName);
    +			}
     		}
     		else if (brokerRelayElem != null) {
     			String prefix = brokerRelayElem.getAttribute("prefix");
    
  • spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket.xsd+17 0 modified
    @@ -384,6 +384,23 @@
                     ]]></xsd:documentation>
     			</xsd:annotation>
     		</xsd:attribute>
    +		<xsd:attribute name="selector-header" type="xsd:string">
    +			<xsd:annotation>
    +				<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
    +	Configure the name of a header that a subscription message can have for
    +	the purpose of filtering messages matched to the subscription. The header
    +	value is expected to be a Spring EL boolean expression to be applied to
    +	the headers of messages matched to the subscription.
    +
    +	For example:
    +	headers.foo == 'bar'
    +
    +	By default this is set to "selector". You can set it to a different
    +	name, or to "" to turn off support for a selector header.
    +                ]]></xsd:documentation>
    +			</xsd:annotation>
    +		</xsd:attribute>
    +
     	</xsd:complexType>
     
     	<xsd:complexType name="channel">
    
  • spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java+5 2 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -47,6 +47,7 @@
     import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
     import org.springframework.messaging.simp.SimpMessagingTemplate;
     import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
    +import org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry;
     import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
     import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
     import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
    @@ -202,6 +203,8 @@ public void simpleBroker() throws Exception {
     		assertNotNull(brokerMessageHandler);
     		Collection<String> prefixes = brokerMessageHandler.getDestinationPrefixes();
     		assertEquals(Arrays.asList("/topic", "/queue"), new ArrayList<>(prefixes));
    +		DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) brokerMessageHandler.getSubscriptionRegistry();
    +		assertEquals("my-selector", registry.getSelectorHeaderName());
     		assertNotNull(brokerMessageHandler.getTaskScheduler());
     		assertArrayEquals(new long[] {15000, 15000}, brokerMessageHandler.getHeartbeatValue());
     
    @@ -228,7 +231,7 @@ public void simpleBroker() throws Exception {
     
     		assertNotNull(this.appContext.getBean("webSocketScopeConfigurer", CustomScopeConfigurer.class));
     
    -		DirectFieldAccessor accessor = new DirectFieldAccessor(brokerMessageHandler.getSubscriptionRegistry());
    +		DirectFieldAccessor accessor = new DirectFieldAccessor(registry);
     		Object pathMatcher = accessor.getPropertyValue("pathMatcher");
     		String pathSeparator = (String) new DirectFieldAccessor(pathMatcher).getPropertyValue("pathSeparator");
     		assertEquals(".", pathSeparator);
    
  • spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml+2 1 modified
    @@ -35,7 +35,8 @@
     
     		<websocket:stomp-error-handler ref="errorHandler" />
     
    -		<websocket:simple-broker prefix="/topic, /queue" heartbeat="15000,15000" scheduler="scheduler" />
    +		<websocket:simple-broker prefix="/topic, /queue" selector-header="my-selector"
    +				heartbeat="15000,15000" scheduler="scheduler" />
     
     	</websocket:message-broker>
     
    
ff2228fdaf13

Selector header name is exposed for configuration

https://github.com/spring-projects/spring-frameworkRossen StoyanchevApr 17, 2018via ghsa
8 files changed · +150 34
  • spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+35 24 modified
    @@ -44,6 +44,7 @@
     import org.springframework.util.LinkedMultiValueMap;
     import org.springframework.util.MultiValueMap;
     import org.springframework.util.PathMatcher;
    +import org.springframework.util.StringUtils;
     
     /**
      * Implementation of {@link SubscriptionRegistry} that stores subscriptions
    @@ -73,6 +74,7 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
     
     	private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
     
    +	@Nullable
     	private String selectorHeaderName = "selector";
     
     	private volatile boolean selectorHeaderInUse = false;
    @@ -114,26 +116,28 @@ public int getCacheLimit() {
     	}
     
     	/**
    -	 * Configure the name of a selector header that a subscription message can
    -	 * have in order to filter messages based on their headers. The value of the
    -	 * header can use Spring EL expressions against message headers.
    -	 * <p>For example the following expression expects a header called "foo" to
    -	 * have the value "bar":
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
     	 * <pre>
     	 * headers.foo == 'bar'
     	 * </pre>
    -	 * <p>By default this is set to "selector".
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
     	 * @since 4.2
     	 */
    -	public void setSelectorHeaderName(String selectorHeaderName) {
    -		Assert.notNull(selectorHeaderName, "'selectorHeaderName' must not be null");
    -		this.selectorHeaderName = selectorHeaderName;
    +	public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
    +		this.selectorHeaderName = StringUtils.hasText(selectorHeaderName) ? selectorHeaderName : null;
     	}
     
     	/**
    -	 * Return the name for the selector header.
    +	 * Return the name for the selector header name.
     	 * @since 4.2
     	 */
    +	@Nullable
     	public String getSelectorHeaderName() {
     		return this.selectorHeaderName;
     	}
    @@ -143,25 +147,32 @@ public String getSelectorHeaderName() {
     	protected void addSubscriptionInternal(
     			String sessionId, String subsId, String destination, Message<?> message) {
     
    +		Expression expression = getSelectorExpression(message.getHeaders());
    +		this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
    +		this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    +	}
    +
    +	@Nullable
    +	private Expression getSelectorExpression(MessageHeaders headers) {
     		Expression expression = null;
    -		MessageHeaders headers = message.getHeaders();
    -		String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
    -		if (selector != null) {
    -			try {
    -				expression = this.expressionParser.parseExpression(selector);
    -				this.selectorHeaderInUse = true;
    -				if (logger.isTraceEnabled()) {
    -					logger.trace("Subscription selector: [" + selector + "]");
    +		if (getSelectorHeaderName() != null) {
    +			String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
    +			if (selector != null) {
    +				try {
    +					expression = this.expressionParser.parseExpression(selector);
    +					this.selectorHeaderInUse = true;
    +					if (logger.isTraceEnabled()) {
    +						logger.trace("Subscription selector: [" + selector + "]");
    +					}
     				}
    -			}
    -			catch (Throwable ex) {
    -				if (logger.isDebugEnabled()) {
    -					logger.debug("Failed to parse selector: " + selector, ex);
    +				catch (Throwable ex) {
    +					if (logger.isDebugEnabled()) {
    +						logger.debug("Failed to parse selector: " + selector, ex);
    +					}
     				}
     			}
     		}
    -		this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
    -		this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
    +		return expression;
     	}
     
     	@Override
    
  • spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java+38 5 modified
    @@ -51,27 +51,32 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
     
     	private static final byte[] EMPTY_PAYLOAD = new byte[0];
     
    -	private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
    -
    -	private SubscriptionRegistry subscriptionRegistry;
     
     	@Nullable
     	private PathMatcher pathMatcher;
     
     	@Nullable
     	private Integer cacheLimit;
     
    +	@Nullable
    +	private String selectorHeaderName = "selector";
    +
     	@Nullable
     	private TaskScheduler taskScheduler;
     
     	@Nullable
     	private long[] heartbeatValue;
     
     	@Nullable
    -	private ScheduledFuture<?> heartbeatFuture;
    +	private MessageHeaderInitializer headerInitializer;
    +
    +
    +	private SubscriptionRegistry subscriptionRegistry;
    +
    +	private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
     
     	@Nullable
    -	private MessageHeaderInitializer headerInitializer;
    +	private ScheduledFuture<?> heartbeatFuture;
     
     
     	/**
    @@ -102,6 +107,7 @@ public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) {
     		this.subscriptionRegistry = subscriptionRegistry;
     		initPathMatcherToUse();
     		initCacheLimitToUse();
    +		initSelectorHeaderNameToUse();
     	}
     
     	public SubscriptionRegistry getSubscriptionRegistry() {
    @@ -149,6 +155,33 @@ private void initCacheLimitToUse() {
     		}
     	}
     
    +	/**
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
    +	 * <pre>
    +	 * headers.foo == 'bar'
    +	 * </pre>
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
    +	 * @since 4.3.17
    +	 * @see #setSubscriptionRegistry
    +	 * @see DefaultSubscriptionRegistry#setSelectorHeaderName(String)
    +	 */
    +	public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
    +		this.selectorHeaderName = selectorHeaderName;
    +		initSelectorHeaderNameToUse();
    +	}
    +
    +	private void initSelectorHeaderNameToUse() {
    +		if (this.subscriptionRegistry instanceof DefaultSubscriptionRegistry) {
    +			((DefaultSubscriptionRegistry) this.subscriptionRegistry).setSelectorHeaderName(this.selectorHeaderName);
    +		}
    +	}
    +
     	/**
     	 * Configure the {@link org.springframework.scheduling.TaskScheduler} to
     	 * use for providing heartbeat support. Setting this property also sets the
    
  • spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java+23 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -36,6 +36,9 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
     	@Nullable
     	private long[] heartbeat;
     
    +	@Nullable
    +	private String selectorHeaderName = "selector";
    +
     
     	public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
     		super(inChannel, outChannel, prefixes);
    @@ -68,6 +71,24 @@ public SimpleBrokerRegistration setHeartbeatValue(long[] heartbeat) {
     		return this;
     	}
     
    +	/**
    +	 * Configure the name of a header that a subscription message can have for
    +	 * the purpose of filtering messages matched to the subscription. The header
    +	 * value is expected to be a Spring EL boolean expression to be applied to
    +	 * the headers of messages matched to the subscription.
    +	 * <p>For example:
    +	 * <pre>
    +	 * headers.foo == 'bar'
    +	 * </pre>
    +	 * <p>By default this is set to "selector". You can set it to a different
    +	 * name, or to {@code null} to turn off support for a selector header.
    +	 * @param selectorHeaderName the name to use for a selector header
    +	 * @since 4.3.17
    +	 */
    +	public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
    +		this.selectorHeaderName = selectorHeaderName;
    +	}
    +
     
     	@Override
     	protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
    @@ -79,6 +100,7 @@ protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel broke
     		if (this.heartbeat != null) {
     			handler.setHeartbeatValue(this.heartbeat);
     		}
    +		handler.setSelectorHeaderName(this.selectorHeaderName);
     		return handler;
     	}
     
    
  • spring-messaging/src/test/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistryTests.java+26 1 modified
    @@ -258,14 +258,16 @@ public void registerSubscriptionWithDestinationPatternRegex() {
     	}
     
     	@Test
    -	public void registerSubscriptionWithSelector() throws Exception {
    +	public void registerSubscriptionWithSelector() {
     		String sessionId = "sess01";
     		String subscriptionId = "subs01";
     		String destination = "/foo";
     		String selector = "headers.foo == 'bar'";
     
     		this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
     
    +		// First, try with selector header
    +
     		SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
     		accessor.setDestination(destination);
     		accessor.setNativeHeader("foo", "bar");
    @@ -276,11 +278,34 @@ public void registerSubscriptionWithSelector() throws Exception {
     		assertEquals(1, actual.size());
     		assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
     
    +		// Then without
    +
     		actual = this.registry.findSubscriptions(createMessage(destination));
     		assertNotNull(actual);
     		assertEquals(0, actual.size());
     	}
     
    +	@Test
    +	public void registerSubscriptionWithSelectorNotSupported() {
    +		String sessionId = "sess01";
    +		String subscriptionId = "subs01";
    +		String destination = "/foo";
    +		String selector = "headers.foo == 'bar'";
    +
    +		this.registry.setSelectorHeaderName(null);
    +		this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
    +
    +		SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
    +		accessor.setDestination(destination);
    +		accessor.setNativeHeader("foo", "bazz");
    +		Message<?> message = MessageBuilder.createMessage("", accessor.getMessageHeaders());
    +
    +		MultiValueMap<String, String> actual = this.registry.findSubscriptions(message);
    +		assertNotNull(actual);
    +		assertEquals(1, actual.size());
    +		assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
    +	}
    +
     	@Test  // SPR-11931
     	public void registerSubscriptionTwiceAndUnregister() {
     		this.registry.registerSubscription(subscribeMessage("sess01", "subs01", "/foo"));
    
  • spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java+4 0 modified
    @@ -383,6 +383,10 @@ private RootBeanDefinition registerMessageBroker(Element brokerElement,
     				String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
     				brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
     			}
    +			if (simpleBrokerElem.hasAttribute("selector-header")) {
    +				String headerName = simpleBrokerElem.getAttribute("selector-header");
    +				brokerDef.getPropertyValues().add("selectorHeaderName", headerName);
    +			}
     		}
     		else if (brokerRelayElem != null) {
     			String prefix = brokerRelayElem.getAttribute("prefix");
    
  • spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket.xsd+17 0 modified
    @@ -384,6 +384,23 @@
                     ]]></xsd:documentation>
     			</xsd:annotation>
     		</xsd:attribute>
    +		<xsd:attribute name="selector-header" type="xsd:string">
    +			<xsd:annotation>
    +				<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
    +	Configure the name of a header that a subscription message can have for
    +	the purpose of filtering messages matched to the subscription. The header
    +	value is expected to be a Spring EL boolean expression to be applied to
    +	the headers of messages matched to the subscription.
    +
    +	For example:
    +	headers.foo == 'bar'
    +
    +	By default this is set to "selector". You can set it to a different
    +	name, or to "" to turn off support for a selector header.
    +                ]]></xsd:documentation>
    +			</xsd:annotation>
    +		</xsd:attribute>
    +
     	</xsd:complexType>
     
     	<xsd:complexType name="channel">
    
  • spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java+5 2 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * 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.
    @@ -47,6 +47,7 @@
     import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
     import org.springframework.messaging.simp.SimpMessagingTemplate;
     import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
    +import org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry;
     import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
     import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
     import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
    @@ -202,6 +203,8 @@ public void simpleBroker() throws Exception {
     		assertNotNull(brokerMessageHandler);
     		Collection<String> prefixes = brokerMessageHandler.getDestinationPrefixes();
     		assertEquals(Arrays.asList("/topic", "/queue"), new ArrayList<>(prefixes));
    +		DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) brokerMessageHandler.getSubscriptionRegistry();
    +		assertEquals("my-selector", registry.getSelectorHeaderName());
     		assertNotNull(brokerMessageHandler.getTaskScheduler());
     		assertArrayEquals(new long[] {15000, 15000}, brokerMessageHandler.getHeartbeatValue());
     
    @@ -228,7 +231,7 @@ public void simpleBroker() throws Exception {
     
     		assertNotNull(this.appContext.getBean("webSocketScopeConfigurer", CustomScopeConfigurer.class));
     
    -		DirectFieldAccessor accessor = new DirectFieldAccessor(brokerMessageHandler.getSubscriptionRegistry());
    +		DirectFieldAccessor accessor = new DirectFieldAccessor(registry);
     		Object pathMatcher = accessor.getPropertyValue("pathMatcher");
     		String pathSeparator = (String) new DirectFieldAccessor(pathMatcher).getPropertyValue("pathSeparator");
     		assertEquals(".", pathSeparator);
    
  • spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml+2 1 modified
    @@ -35,7 +35,8 @@
     
     		<websocket:stomp-error-handler ref="errorHandler" />
     
    -		<websocket:simple-broker prefix="/topic, /queue" heartbeat="15000,15000" scheduler="scheduler" />
    +		<websocket:simple-broker prefix="/topic, /queue" selector-header="my-selector"
    +				heartbeat="15000,15000" scheduler="scheduler" />
     
     	</websocket:message-broker>
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

15

News mentions

0

No linked articles in our index yet.