Skip to content

Commit 85fa7bb

Browse files
authored
Restrict URL protocol types loaded by XBeanBrokerFactory (#1910)
This adds a new system property to control which protocol types are valid for loading resources using the XBeanBrokerFactory. By default only file and classpath resources can be loaded. The goal of this is to prevent possible future security issues by hardening what is allowed to be loaded by default.
1 parent a04c088 commit 85fa7bb

File tree

6 files changed

+808
-18
lines changed

6 files changed

+808
-18
lines changed

activemq-spring/src/main/java/org/apache/activemq/spring/Utils.java

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import java.io.File;
2020
import java.io.FileNotFoundException;
2121
import java.net.MalformedURLException;
22+
import java.net.URI;
23+
import java.net.URISyntaxException;
24+
import java.util.Set;
2225
import org.springframework.core.io.ClassPathResource;
2326
import org.springframework.core.io.FileSystemResource;
2427
import org.springframework.core.io.Resource;
@@ -27,22 +30,69 @@
2730

2831
public class Utils {
2932

33+
public static final String FILE_PROTOCOL = "file";
34+
public static final String CLASSPATH_PROTOCOL = "classpath";
35+
3036
public static Resource resourceFromString(String uri) throws MalformedURLException {
31-
Resource resource;
32-
File file = new File(uri);
33-
if (file.exists()) {
37+
// default allows all
38+
return resourceFromString(uri, null);
39+
}
40+
41+
public static Resource resourceFromString(String uri, Set<String> allowedProtocols) throws MalformedURLException {
42+
// Empty set means nothing is allowed
43+
if (allowedProtocols != null && allowedProtocols.isEmpty()) {
44+
throw new IllegalArgumentException("No protocols are allowed for loading resources.");
45+
}
46+
47+
final Resource resource;
48+
49+
// First, just try and load a local file (if it exists) and if "file"
50+
// as part of the allow list. This preserves previous behavior of
51+
// always optimistically trying a local file first.
52+
if (isAllowFile(allowedProtocols) && new File(uri).exists()) {
3453
resource = new FileSystemResource(uri);
54+
// If file isn't allowed, or if the file can't be found then check
55+
// if the string is a valid URL. If it's valid, then we need
56+
// to validate if it's allowed before loading the URL.
57+
// isUrl() uses URI internally so it won't actually load anything
3558
} else if (ResourceUtils.isUrl(uri)) {
3659
try {
60+
validateUrlAllowed(uri, allowedProtocols);
3761
resource = new UrlResource(ResourceUtils.getURL(uri));
38-
} catch (FileNotFoundException e) {
62+
} catch (FileNotFoundException | URISyntaxException e) {
3963
MalformedURLException malformedURLException = new MalformedURLException(uri);
4064
malformedURLException.initCause(e);
41-
throw malformedURLException;
65+
throw malformedURLException;
4266
}
43-
} else {
67+
// Fallback to trying on the classpath if not a valid Url, and we allow it which
68+
// also preserves the previous behavior (if classpath is allowed)
69+
} else if (isAllowClasspath(allowedProtocols)){
4470
resource = new ClassPathResource(uri);
71+
// Catch all fail-safe if nothing else matches. This could happen if file is allowed
72+
// but not classpath but the file doesn't exist
73+
} else {
74+
throw new IllegalArgumentException("URL [" + uri + "] can't be found or is not allowed"
75+
+ " for loading resources");
4576
}
4677
return resource;
4778
}
79+
80+
static boolean isAllowFile(Set<String> allowedProtocols) {
81+
return allowedProtocols == null || allowedProtocols.contains(FILE_PROTOCOL);
82+
}
83+
84+
static boolean isAllowClasspath(Set<String> allowedProtocols) {
85+
return allowedProtocols == null || allowedProtocols.contains(CLASSPATH_PROTOCOL);
86+
}
87+
88+
private static void validateUrlAllowed(String uriString, Set<String> allowedProtocols)
89+
throws URISyntaxException {
90+
// Use new URI() to get the scheme
91+
// This is important because ResourceUtils.getURL() actually searches
92+
// the classpath which we don't want to do if not allowed
93+
if (allowedProtocols != null && !allowedProtocols.contains(new URI(uriString).getScheme())) {
94+
throw new IllegalArgumentException("URL [" + uriString +
95+
"] does not use an allowed protocol for loading URL resources");
96+
}
97+
}
4898
}

activemq-spring/src/main/java/org/apache/activemq/xbean/XBeanBrokerFactory.java

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import java.net.MalformedURLException;
2121
import java.net.URI;
2222

23-
import org.apache.activemq.broker.BrokerContextAware;
23+
import java.util.Arrays;
24+
import java.util.Set;
25+
import java.util.stream.Collectors;
2426
import org.apache.activemq.broker.BrokerFactoryHandler;
2527
import org.apache.activemq.broker.BrokerService;
2628
import org.apache.activemq.spring.SpringBrokerContext;
@@ -35,20 +37,38 @@
3537
import org.springframework.beans.FatalBeanException;
3638
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
3739
import org.springframework.context.ApplicationContext;
38-
import org.springframework.context.ApplicationContextAware;
3940
import org.springframework.core.io.Resource;
4041

4142
/**
4243
*
4344
*/
4445
public class XBeanBrokerFactory implements BrokerFactoryHandler {
45-
private static final transient Logger LOG = LoggerFactory.getLogger(XBeanBrokerFactory.class);
46+
private static final Logger LOG = LoggerFactory.getLogger(XBeanBrokerFactory.class);
47+
48+
public static final String XBEAN_BROKER_FACTORY_PROTOCOLS_PROP =
49+
"org.apache.activemq.xbean.XBEAN_BROKER_FACTORY_PROTOCOLS";
50+
public static final String DEFAULT_ALLOWED_PROTOCOLS = Utils.FILE_PROTOCOL + "," + Utils.CLASSPATH_PROTOCOL;
51+
52+
private final Set<String> allowedProtocols;
4653

4754
static {
4855
PropertyEditorManager.registerEditor(URI.class, URIEditor.class);
4956
}
5057

58+
public XBeanBrokerFactory() {
59+
final String allowedProtocols = System.getProperty(XBEAN_BROKER_FACTORY_PROTOCOLS_PROP,
60+
DEFAULT_ALLOWED_PROTOCOLS);
61+
62+
// Asterisk will map to null which will allow all and skip checking
63+
// Empty string will map to an empty set and will deny all
64+
this.allowedProtocols = !allowedProtocols.equals("*") ?
65+
Arrays.stream(allowedProtocols.split("\\s*,\\s*"))
66+
.filter(s -> !s.isBlank())
67+
.collect(Collectors.toUnmodifiableSet()) : null;
68+
}
69+
5170
private boolean validate = true;
71+
5272
public boolean isValidate() {
5373
return validate;
5474
}
@@ -75,12 +95,10 @@ public BrokerService createBroker(URI config) throws Exception {
7595
if (broker == null) {
7696
// lets try find by type
7797
String[] names = context.getBeanNamesForType(BrokerService.class);
78-
for (int i = 0; i < names.length; i++) {
79-
String name = names[i];
80-
broker = (BrokerService)context.getBean(name);
81-
if (broker != null) {
82-
break;
83-
}
98+
for (String name : names) {
99+
// No need to check for null, this will throw an exception if not found
100+
broker = (BrokerService) context.getBean(name);
101+
break;
84102
}
85103
}
86104
if (broker == null) {
@@ -98,8 +116,8 @@ public BrokerService createBroker(URI config) throws Exception {
98116
}
99117

100118
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
101-
Resource resource = Utils.resourceFromString(uri);
102-
LOG.debug("Using " + resource + " from " + uri);
119+
Resource resource = Utils.resourceFromString(uri, allowedProtocols);
120+
LOG.debug("Using {} from {}", resource, uri);
103121
try {
104122
return new ResourceXmlApplicationContext(resource) {
105123
@Override
@@ -108,9 +126,14 @@ protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) {
108126
}
109127
};
110128
} catch (FatalBeanException errorToLog) {
111-
LOG.error("Failed to load: " + resource + ", reason: " + errorToLog.getLocalizedMessage(), errorToLog);
129+
LOG.error("Failed to load: {}, reason: {}", resource, errorToLog.getLocalizedMessage(),
130+
errorToLog);
112131
throw errorToLog;
113132
}
114133
}
115134

135+
// Package scope for testing
136+
Set<String> getAllowedProtocols() {
137+
return allowedProtocols;
138+
}
116139
}

0 commit comments

Comments
 (0)