This is an automated email from the ASF dual-hosted git repository.

cshannon pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq.git


The following commit(s) were added to refs/heads/main by this push:
     new c9d3963f20 Improve FactoryFinder Validation (#1793)
c9d3963f20 is described below

commit c9d3963f20577cd1995fa2b442ec15f8c806e932
Author: Christopher L. Shannon <[email protected]>
AuthorDate: Wed Mar 18 09:40:15 2026 -0400

    Improve FactoryFinder Validation (#1793)
    
    Also modernizes with generics and adds improved testing
---
 .../org/apache/activemq/broker/BrokerFactory.java  |   6 +-
 .../broker/region/group/GroupFactoryFinder.java    |   6 +-
 .../transport/auto/AutoTcpTransportServer.java     |  12 +-
 .../org/apache/activemq/util/osgi/Activator.java   |  23 ++-
 .../activemq/transport/TransportFactory.java       |  12 +-
 .../transport/discovery/DiscoveryAgentFactory.java |   6 +-
 .../org/apache/activemq/util/FactoryFinder.java    | 181 +++++++++++++---
 .../apache/activemq/util/FactoryFinderTest.java    | 202 ++++++++++++++++++
 .../store/jdbc/JDBCPersistenceAdapter.java         |  10 +-
 .../transport/mqtt/MQTTProtocolConverter.java      |   8 +-
 activemq-spring/pom.xml                            |   2 +-
 .../transport/stomp/ProtocolConverter.java         |  75 ++++---
 .../transport/stomp/ProtocolConverterTest.java     | 192 +++++++++++++++++
 .../apache/activemq/transport/stomp/StompTest.java | 117 +++++++++++
 .../activemq/transport/frametranslator/stomp-test  |  17 ++
 .../transport/frametranslator/stomp-test-invalid   |  17 ++
 .../apache/activemq/web/QueueBrowseServlet.java    |  31 +--
 .../activemq/web/QueueBrowseServletTest.java       | 230 +++++++++++++++++++++
 .../org/apache/activemq/web/view/invalid-view      |  19 ++
 .../org/apache/activemq/web/view/valid-view        |  19 ++
 20 files changed, 1087 insertions(+), 98 deletions(-)

diff --git 
a/activemq-broker/src/main/java/org/apache/activemq/broker/BrokerFactory.java 
b/activemq-broker/src/main/java/org/apache/activemq/broker/BrokerFactory.java
index 9dfb85952e..3d635edf9a 100644
--- 
a/activemq-broker/src/main/java/org/apache/activemq/broker/BrokerFactory.java
+++ 
b/activemq-broker/src/main/java/org/apache/activemq/broker/BrokerFactory.java
@@ -31,14 +31,16 @@ import org.apache.activemq.util.IOExceptionSupport;
  */
 public final class BrokerFactory {
 
-    private static final FactoryFinder BROKER_FACTORY_HANDLER_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/broker/");
+    private static final FactoryFinder<BrokerFactoryHandler> 
BROKER_FACTORY_HANDLER_FINDER
+            = new 
FactoryFinder<>("META-INF/services/org/apache/activemq/broker/", 
BrokerFactoryHandler.class,
+            null);
 
     private BrokerFactory() {        
     }
     
     public static BrokerFactoryHandler createBrokerFactoryHandler(String type) 
throws IOException {
         try {
-            return 
(BrokerFactoryHandler)BROKER_FACTORY_HANDLER_FINDER.newInstance(type);
+            return BROKER_FACTORY_HANDLER_FINDER.newInstance(type);
         } catch (Throwable e) {
             throw IOExceptionSupport.create("Could not load " + type + " 
factory:" + e, e);
         }
diff --git 
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/group/GroupFactoryFinder.java
 
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/group/GroupFactoryFinder.java
index 718b37d708..78092eb316 100644
--- 
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/group/GroupFactoryFinder.java
+++ 
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/group/GroupFactoryFinder.java
@@ -25,7 +25,9 @@ import org.apache.activemq.util.IntrospectionSupport;
 import org.apache.activemq.util.URISupport;
 
 public class GroupFactoryFinder {
-    private static final FactoryFinder GROUP_FACTORY_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/groups/");
+    private static final FactoryFinder<MessageGroupMapFactory> 
GROUP_FACTORY_FINDER =
+            new 
FactoryFinder<>("META-INF/services/org/apache/activemq/groups/", 
MessageGroupMapFactory.class,
+                    null);
 
     private GroupFactoryFinder() {
     }
@@ -40,7 +42,7 @@ public class GroupFactoryFinder {
                 factoryType = factoryType.substring(0,p);
                 properties = URISupport.parseQuery(propertiesString);
             }
-            MessageGroupMapFactory result =  
(MessageGroupMapFactory)GROUP_FACTORY_FINDER.newInstance(factoryType);
+            MessageGroupMapFactory result = 
GROUP_FACTORY_FINDER.newInstance(factoryType);
             if (properties != null && result != null){
                 IntrospectionSupport.setProperties(result,properties);
             }
diff --git 
a/activemq-broker/src/main/java/org/apache/activemq/transport/auto/AutoTcpTransportServer.java
 
b/activemq-broker/src/main/java/org/apache/activemq/transport/auto/AutoTcpTransportServer.java
index bcd73653d0..b2ea7c06c1 100644
--- 
a/activemq-broker/src/main/java/org/apache/activemq/transport/auto/AutoTcpTransportServer.java
+++ 
b/activemq-broker/src/main/java/org/apache/activemq/transport/auto/AutoTcpTransportServer.java
@@ -80,15 +80,19 @@ public class AutoTcpTransportServer extends 
TcpTransportServer {
     protected int maxConnectionThreadPoolSize = Integer.MAX_VALUE;
     protected int protocolDetectionTimeOut = 30000;
 
-    private static final FactoryFinder TRANSPORT_FACTORY_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/transport/");
+    private static final FactoryFinder<TransportFactory> 
TRANSPORT_FACTORY_FINDER =
+            new 
FactoryFinder<>("META-INF/services/org/apache/activemq/transport/", 
TransportFactory.class,
+                    null);
     private final ConcurrentMap<String, TransportFactory> transportFactories = 
new ConcurrentHashMap<String, TransportFactory>();
 
-    private static final FactoryFinder WIREFORMAT_FACTORY_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/wireformat/");
+    private static final FactoryFinder<WireFormatFactory> 
WIREFORMAT_FACTORY_FINDER =
+            new 
FactoryFinder<>("META-INF/services/org/apache/activemq/wireformat/", 
WireFormatFactory.class,
+                    null);
 
     public WireFormatFactory findWireFormatFactory(String scheme, Map<String, 
Map<String, Object>> options) throws IOException {
         WireFormatFactory wff = null;
         try {
-            wff = 
(WireFormatFactory)WIREFORMAT_FACTORY_FINDER.newInstance(scheme);
+            wff = WIREFORMAT_FACTORY_FINDER.newInstance(scheme);
             if (options != null) {
                 final Map<String, Object> wfOptions = new HashMap<>();
                 if (options.get(AutoTransportUtils.ALL) != null) {
@@ -117,7 +121,7 @@ public class AutoTcpTransportServer extends 
TcpTransportServer {
         if (tf == null) {
             // Try to load if from a META-INF property.
             try {
-                tf = 
(TransportFactory)TRANSPORT_FACTORY_FINDER.newInstance(scheme);
+                tf = TRANSPORT_FACTORY_FINDER.newInstance(scheme);
                 if (options != null) {
                     IntrospectionSupport.setProperties(tf, options);
                 }
diff --git 
a/activemq-broker/src/main/java/org/apache/activemq/util/osgi/Activator.java 
b/activemq-broker/src/main/java/org/apache/activemq/util/osgi/Activator.java
index 00ca2154b3..ddc2306c9b 100644
--- a/activemq-broker/src/main/java/org/apache/activemq/util/osgi/Activator.java
+++ b/activemq-broker/src/main/java/org/apache/activemq/util/osgi/Activator.java
@@ -172,7 +172,15 @@ public class Activator implements BundleActivator, 
SynchronousBundleListener, Ob
     // ================================================================
 
     @Override
-    public Object create(String path) throws IllegalAccessException, 
InstantiationException, IOException, ClassNotFoundException {
+    public Object create(String path)
+            throws IllegalAccessException, InstantiationException, 
IOException, ClassNotFoundException {
+        throw new UnsupportedOperationException("Create is not supported 
without requiredType and allowed impls");
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T create(String path, Class<T> requiredType, Set<Class<? 
extends T>> allowedImpls)
+            throws IllegalAccessException, InstantiationException, 
IOException, ClassNotFoundException {
         Class<?> clazz = serviceCache.get(path);
         if (clazz == null) {
             StringBuilder warnings = new StringBuilder();
@@ -199,6 +207,10 @@ public class Activator implements BundleActivator, 
SynchronousBundleListener, Ob
                     continue;
                 }
 
+                // no reason to cache if invalid so validate before caching
+                // the class inside of serviceCache
+                FactoryFinder.validateClass(clazz, requiredType, allowedImpls);
+
                 // Yay.. the class was found.  Now cache it.
                 serviceCache.put(path, clazz);
                 wrapper.cachedServices.add(path);
@@ -214,10 +226,17 @@ public class Activator implements BundleActivator, 
SynchronousBundleListener, Ob
                 }
                 throw new IOException(msg);
             }
+        } else {
+            // Validate again (even for previously cached classes) in case
+            // a path is re-used with a different requiredType.
+            // This object factory is shared by all factory finder instances, 
so it would be
+            // possible (although probably a mistake) to use the same
+            // path again with a different requiredType in a different 
FactoryFinder
+            FactoryFinder.validateClass(clazz, requiredType, allowedImpls);
         }
         
         try {
-            return clazz.getConstructor().newInstance();
+            return (T) clazz.getConstructor().newInstance();
         } catch (InvocationTargetException | NoSuchMethodException e) {
                throw new InstantiationException(e.getMessage());
         }
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/transport/TransportFactory.java
 
b/activemq-client/src/main/java/org/apache/activemq/transport/TransportFactory.java
index acfc1b0389..184a5d6391 100644
--- 
a/activemq-client/src/main/java/org/apache/activemq/transport/TransportFactory.java
+++ 
b/activemq-client/src/main/java/org/apache/activemq/transport/TransportFactory.java
@@ -36,8 +36,12 @@ import org.apache.activemq.wireformat.WireFormatFactory;
 
 public abstract class TransportFactory {
 
-    private static final FactoryFinder TRANSPORT_FACTORY_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/transport/");
-    private static final FactoryFinder WIREFORMAT_FACTORY_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/wireformat/");
+    private static final FactoryFinder<TransportFactory> 
TRANSPORT_FACTORY_FINDER
+            = new 
FactoryFinder<>("META-INF/services/org/apache/activemq/transport/", 
TransportFactory.class,
+            null);
+    private static final FactoryFinder<WireFormatFactory> 
WIREFORMAT_FACTORY_FINDER
+            = new 
FactoryFinder<>("META-INF/services/org/apache/activemq/wireformat/", 
WireFormatFactory.class,
+            null);
     private static final ConcurrentMap<String, TransportFactory> 
TRANSPORT_FACTORYS = new ConcurrentHashMap<String, TransportFactory>();
 
     private static final String WRITE_TIMEOUT_FILTER = "soWriteTimeout";
@@ -179,7 +183,7 @@ public abstract class TransportFactory {
         if (tf == null) {
             // Try to load if from a META-INF property.
             try {
-                tf = 
(TransportFactory)TRANSPORT_FACTORY_FINDER.newInstance(scheme);
+                tf = TRANSPORT_FACTORY_FINDER.newInstance(scheme);
                 TRANSPORT_FACTORYS.put(scheme, tf);
             } catch (Throwable e) {
                 throw IOExceptionSupport.create("Transport scheme NOT 
recognized: [" + scheme + "]", e);
@@ -201,7 +205,7 @@ public abstract class TransportFactory {
         }
 
         try {
-            WireFormatFactory wff = 
(WireFormatFactory)WIREFORMAT_FACTORY_FINDER.newInstance(wireFormat);
+            WireFormatFactory wff = 
WIREFORMAT_FACTORY_FINDER.newInstance(wireFormat);
             IntrospectionSupport.setProperties(wff, options, "wireFormat.");
             return wff;
         } catch (Throwable e) {
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/transport/discovery/DiscoveryAgentFactory.java
 
b/activemq-client/src/main/java/org/apache/activemq/transport/discovery/DiscoveryAgentFactory.java
index 65f3ee6933..46894d4729 100644
--- 
a/activemq-client/src/main/java/org/apache/activemq/transport/discovery/DiscoveryAgentFactory.java
+++ 
b/activemq-client/src/main/java/org/apache/activemq/transport/discovery/DiscoveryAgentFactory.java
@@ -26,7 +26,9 @@ import org.apache.activemq.util.IOExceptionSupport;
 
 public abstract class DiscoveryAgentFactory {
 
-    private static final FactoryFinder DISCOVERY_AGENT_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/transport/discoveryagent/");
+    private static final FactoryFinder<DiscoveryAgentFactory> 
DISCOVERY_AGENT_FINDER =
+            new 
FactoryFinder<>("META-INF/services/org/apache/activemq/transport/discoveryagent/",
+                    DiscoveryAgentFactory.class, null);
     private static final ConcurrentMap<String, DiscoveryAgentFactory> 
DISCOVERY_AGENT_FACTORYS = new ConcurrentHashMap<String, 
DiscoveryAgentFactory>();
 
     /**
@@ -43,7 +45,7 @@ public abstract class DiscoveryAgentFactory {
         if (daf == null) {
             // Try to load if from a META-INF property.
             try {
-                daf = 
(DiscoveryAgentFactory)DISCOVERY_AGENT_FINDER.newInstance(scheme);
+                daf = DISCOVERY_AGENT_FINDER.newInstance(scheme);
                 DISCOVERY_AGENT_FACTORYS.put(scheme, daf);
             } catch (Throwable e) {
                 throw IOExceptionSupport.create("DiscoveryAgent scheme NOT 
recognized: [" + scheme + "]", e);
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/util/FactoryFinder.java 
b/activemq-client/src/main/java/org/apache/activemq/util/FactoryFinder.java
index 596d15ea00..594af7e257 100644
--- a/activemq-client/src/main/java/org/apache/activemq/util/FactoryFinder.java
+++ b/activemq-client/src/main/java/org/apache/activemq/util/FactoryFinder.java
@@ -20,14 +20,22 @@ import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
+import java.net.UnknownServiceException;
+import java.nio.file.Path;
+import java.security.Security;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
 
 /**
  *
  */
-public class FactoryFinder {
+public class FactoryFinder<T> {
 
     /**
      * The strategy that the FactoryFinder uses to find load and instantiate 
Objects
@@ -44,51 +52,66 @@ public class FactoryFinder {
          * @param path the full service path
          * @return
          */
-        public Object create(String path) throws IllegalAccessException, 
InstantiationException, IOException, ClassNotFoundException;
+        Object create(String path) throws IllegalAccessException, 
InstantiationException, IOException, ClassNotFoundException;
 
+        /**
+         * This method loads objects by searching for classes in the given 
path.
+         * A requiredType and Set of allowed implementations are provided for
+         * implementations to use for validation. Note is up to the actual 
implementations
+         * that implement {@link ObjectFactory} to decide how to use both 
parameters.
+         * By default, the method just delegates to {@link #create(String)}
+         *
+         * @param path the full service path
+         * @param requiredType the requiredType any objects must implement
+         * @param allowedImpls The set of allowed impls
+         * @return
+         */
+        @SuppressWarnings("unchecked")
+        default <T> T create(String path, Class<T> requiredType, Set<Class<? 
extends T>> allowedImpls)
+                throws IllegalAccessException, InstantiationException, 
IOException, ClassNotFoundException {
+            return (T) create(path);
+        }
     }
 
     /**
      * The default implementation of Object factory which works well in 
standalone applications.
      */
     protected static class StandaloneObjectFactory implements ObjectFactory {
-        final ConcurrentMap<String, Class> classMap = new 
ConcurrentHashMap<String, Class>();
+        final ConcurrentMap<String, Class<?>> classMap = new 
ConcurrentHashMap<>();
+
+        @Override
+        public Object create(final String path)
+                throws IllegalAccessException, InstantiationException, 
IOException, ClassNotFoundException {
+            throw new UnsupportedOperationException("Create is not supported 
without requiredType and allowed impls");
+        }
 
+        @SuppressWarnings("unchecked")
         @Override
-        public Object create(final String path) throws InstantiationException, 
IllegalAccessException, ClassNotFoundException, IOException {
-            Class clazz = classMap.get(path);
+        public <T> T create(String path, Class<T> requiredType, Set<Class<? 
extends T>> allowedImpls) throws InstantiationException, 
IllegalAccessException, ClassNotFoundException, IOException {
+            Class<?> clazz = classMap.get(path);
             if (clazz == null) {
                 clazz = loadClass(loadProperties(path));
+                // no reason to cache if invalid so validate before caching
+                validateClass(clazz, requiredType, allowedImpls);
                 classMap.put(path, clazz);
+            } else {
+                // Validate again (even for previously cached classes) in case
+                // a path is re-used with a different requiredType.
+                // This object factory is static and shared by all factory 
finder instances by default,
+                // so it would be possible (although probably a mistake) to 
use the same
+                // path again with a different requiredType in a different 
FactoryFinder
+                validateClass(clazz, requiredType, allowedImpls);
             }
-            
+
             try {
-               return clazz.getConstructor().newInstance();
+               return (T) clazz.getConstructor().newInstance();
             } catch (NoSuchMethodException | InvocationTargetException e) {
                throw new InstantiationException(e.getMessage());
             }
         }
 
-        static public Class loadClass(Properties properties) throws 
ClassNotFoundException, IOException {
-
-            String className = properties.getProperty("class");
-            if (className == null) {
-                throw new IOException("Expected property is missing: class");
-            }
-            Class clazz = null;
-            ClassLoader loader = 
Thread.currentThread().getContextClassLoader();
-            if (loader != null) {
-                try {
-                    clazz = loader.loadClass(className);
-                } catch (ClassNotFoundException e) {
-                    // ignore
-                }
-            }
-            if (clazz == null) {
-                clazz = 
FactoryFinder.class.getClassLoader().loadClass(className);
-            }
-
-            return clazz;
+        static Class<?> loadClass(Properties properties) throws 
ClassNotFoundException, IOException {
+            return FactoryFinder.loadClass(properties.getProperty("class"));
         }
 
         static public Properties loadProperties(String uri) throws IOException 
{
@@ -138,11 +161,45 @@ public class FactoryFinder {
     // Instance methods and properties
     // ================================================================
     private final String path;
+    private final Class<T> requiredType;
+    private final Set<Class<? extends T>> allowedImpls;
 
-    public FactoryFinder(String path) {
-        this.path = path;
+    /**
+     *
+     * @param path The path to search for impls
+     * @param requiredType Required interface type that any impl must implement
+     * @param allowedImpls The list of allowed implementations. If null or 
asterisk
+     * then all impls of the requiredType are allowed.
+     */
+    public FactoryFinder(String path, Class<T> requiredType, String 
allowedImpls) {
+        this.path = Objects.requireNonNull(path);
+        this.requiredType = Objects.requireNonNull(requiredType);
+        this.allowedImpls = loadAllowedImpls(requiredType, allowedImpls);
     }
 
+    @SuppressWarnings("unchecked")
+    private static <T> Set<Class<? extends T>> loadAllowedImpls(Class<T> 
requiredType, String allowedImpls) {
+        // If allowedImpls is either null or an asterisk (allow all wild card) 
then set to null so we don't filter
+        // If allowedImpls is only an empty string we return an empty set 
meaning allow none
+        // Otherwise split/trim all values
+        return allowedImpls != null && !allowedImpls.equals("*") ?
+            Arrays.stream(allowedImpls.split("\\s*,\\s*"))
+                .filter(s -> !s.isEmpty())
+                .map(s -> {
+                    try {
+                        final Class<?> clazz = FactoryFinder.loadClass(s);
+                        if (!requiredType.isAssignableFrom(clazz)) {
+                            throw new IllegalArgumentException(
+                                    "Class " + clazz + " is not assignable to 
" + requiredType);
+                        }
+                        return (Class<? extends T>)clazz;
+                    } catch (ClassNotFoundException | IOException e) {
+                        throw new IllegalArgumentException(e);
+                    }
+                }).collect(Collectors.toUnmodifiableSet()) : null;
+    }
+
+
     /**
      * Creates a new instance of the given key
      *
@@ -150,9 +207,71 @@ public class FactoryFinder {
      *                the factory name
      * @return a newly created instance
      */
-    public Object newInstance(String key) throws IllegalAccessException, 
InstantiationException, IOException, ClassNotFoundException {
-        return objectFactory.create(path+key);
+    public T newInstance(String key) throws IllegalAccessException, 
InstantiationException, IOException, ClassNotFoundException {
+        return objectFactory.create(resolvePath(key), requiredType, 
allowedImpls);
     }
 
+    Set<Class<? extends T>> getAllowedImpls() {
+        return allowedImpls;
+    }
+
+    Class<T> getRequiredType() {
+        return requiredType;
+    }
+
+    private String resolvePath(final String key) throws InstantiationException 
{
+        // Normalize the base path with the given key. This
+        // will resolve/remove any relative ".." sections of the path.
+        // Example: "/dir1/dir2/dir3/../file" becomes "/dir1/dir2/file"
+        final Path resolvedPath = Path.of(path).resolve(key).normalize();
+
+        // Validate the resolved path is still within the original defined
+        // root path and throw an error of it is not.
+        if (!resolvedPath.startsWith(path)) {
+            throw new InstantiationException("Provided key escapes the 
FactoryFinder configured directory");
+        }
+
+        return resolvedPath.toString();
+    }
+
+    public static String buildAllowedImpls(Class<?>...classes) {
+        return Arrays.stream(Objects.requireNonNull(classes, "List of allowed 
classes may not be null"))
+                .map(Class::getName).collect(Collectors.joining(","));
+    }
+
+    public static <T> void validateClass(Class<?> clazz, Class<T> requiredType,
+            Set<Class<? extends T>> allowedImpls) throws 
InstantiationException {
+        // Validate the loaded class is an allowed impl
+        if (allowedImpls != null && !allowedImpls.contains(clazz)) {
+            throw new InstantiationException("Class " + clazz + " is not an 
allowed implementation "
+                    + "of type " + requiredType);
+        }
+        // Validate the loaded class is a subclass of the right type
+        // The allowedImpls may not be used so also check requiredType. Even 
if set
+        // generics can be erased and this is an extra safety check
+        if (!requiredType.isAssignableFrom(clazz)) {
+            throw new InstantiationException("Class " + clazz + " is not 
assignable to " + requiredType);
+        }
+    }
+
+    static Class<?> loadClass(String className) throws ClassNotFoundException, 
IOException {
+        if (className == null) {
+            throw new IOException("Expected property is missing: class");
+        }
+        Class<?> clazz = null;
+        ClassLoader loader = Thread.currentThread().getContextClassLoader();
+        if (loader != null) {
+            try {
+                clazz = loader.loadClass(className);
+            } catch (ClassNotFoundException e) {
+                // ignore
+            }
+        }
+        if (clazz == null) {
+            clazz = FactoryFinder.class.getClassLoader().loadClass(className);
+        }
+
+        return clazz;
+    }
 
 }
diff --git 
a/activemq-client/src/test/java/org/apache/activemq/util/FactoryFinderTest.java 
b/activemq-client/src/test/java/org/apache/activemq/util/FactoryFinderTest.java
new file mode 100644
index 0000000000..3e775b2161
--- /dev/null
+++ 
b/activemq-client/src/test/java/org/apache/activemq/util/FactoryFinderTest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.activemq.transport.TransportFactory;
+import org.apache.activemq.transport.nio.NIOTransportFactory;
+import org.apache.activemq.transport.tcp.SslTransport;
+import org.apache.activemq.transport.tcp.SslTransportFactory;
+import org.apache.activemq.transport.tcp.TcpTransportFactory;
+import org.apache.activemq.util.FactoryFinder.ObjectFactory;
+import org.apache.activemq.wireformat.WireFormatFactory;
+import org.junit.Test;
+
+public class FactoryFinderTest {
+
+    private static final String TRANSPORT_FACTORY_PATH = 
"META-INF/services/org/apache/activemq/transport/";
+    private static final String WIREFORMAT_FACTORY_PATH = 
"META-INF/services/org/apache/activemq/wireformat/";
+
+    // Test path traversal attempts will throw an error
+    @Test
+    public void testPathTraversal() throws Exception {
+        FactoryFinder<TransportFactory> finder
+                = new FactoryFinder<>(TRANSPORT_FACTORY_PATH, 
TransportFactory.class, null);
+        assertNull(finder.getAllowedImpls());
+
+        try {
+            finder.newInstance("../../tcp");
+            fail("should have failed instantiation");
+        } catch (InstantiationException e) {
+            assertEquals("Provided key escapes the FactoryFinder configured 
directory",
+                    e.getMessage());
+        }
+    }
+
+    // WireFormatFactory is not assignable to TransportFactory
+    // So the constructor should throw an IllegalArgumentException
+    @Test(expected = IllegalArgumentException.class)
+    public void testAllowedImplsMatchInterface() {
+        new FactoryFinder<>(TRANSPORT_FACTORY_PATH, TransportFactory.class,
+                FactoryFinder.buildAllowedImpls(WireFormatFactory.class));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testAllowedImplsNotFound() {
+        new FactoryFinder<>(TRANSPORT_FACTORY_PATH, TransportFactory.class, 
"some.invalid.ClassName");
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadClassNull() throws IOException, ClassNotFoundException 
{
+        FactoryFinder.loadClass(null);
+    }
+
+    @Test(expected = ClassNotFoundException.class)
+    public void testLoadClassNotFound() throws IOException, 
ClassNotFoundException {
+        FactoryFinder.loadClass("some.invalid.ClassName");
+    }
+
+    // The default factory should throw UnsupportedOperationException
+    // if using legacy create() method
+    @Test(expected = UnsupportedOperationException.class)
+    public void testDefaultObjectFinder() throws Exception {
+        ObjectFactory factory = FactoryFinder.getObjectFactory();
+        factory.create("path");
+    }
+
+    @Test
+    public void testObjectFinderDefaultMethod() throws Exception {
+        AtomicBoolean called = new AtomicBoolean();
+        ObjectFactory factory = path -> {
+            called.set(true);
+            return null;
+        };
+        // test that new method defaults to legacy method if not implemented
+        factory.create(null, null, null);
+        assertTrue(called.get());
+    }
+
+    // Verify interface check works if not using an allowed list to at least 
verify that
+    // any classes loaded will implement/extend the required type
+    @Test
+    public void testInterfaceMismatch() throws Exception {
+        // use wrong interface type, WIREFORMAT_FACTORY_PATH should be of type 
WireFormatFactory
+        // and not TransportFactory
+        FactoryFinder<TransportFactory> factory = new 
FactoryFinder<>(WIREFORMAT_FACTORY_PATH, TransportFactory.class,
+                null);
+        // This is a valid impl in the wireformat directory, but it isn't a 
TransportFactory type
+        try {
+            factory.newInstance("default");
+        } catch (InstantiationException e) {
+            assertTrue(e.getMessage().contains("OpenWireFormatFactory is not 
assignable to class " +
+                    TransportFactory.class.getName()));
+        }
+    }
+
+    // Test filtering by allowed implementations of the given interface
+    @Test
+    public void testAllowedImplsFilter() throws Exception {
+        Set<Class<?>> allowedImpls = Set.of(TcpTransportFactory.class, 
NIOTransportFactory.class);
+        FactoryFinder<TransportFactory> finder = new 
FactoryFinder<>(TRANSPORT_FACTORY_PATH, TransportFactory.class,
+                FactoryFinder.buildAllowedImpls(allowedImpls.toArray(new 
Class<?>[0])));
+        assertEquals(TransportFactory.class, finder.getRequiredType());
+        assertEquals(allowedImpls, finder.getAllowedImpls());
+        // tcp and nio are the only class in the allow list
+        assertNotNull(finder.newInstance("tcp"));
+
+        try {
+            // ssl transport factory was not in the allow list
+            finder.newInstance("ssl");
+            fail("should have failed instantiation");
+        } catch (InstantiationException e) {
+            assertTrue(e.getMessage().contains("is not an allowed 
implementation of type"));
+        }
+    }
+
+    // empty array is used so we should deny everything
+    @Test
+    public void testAllowedImplsFilterDenyAll() throws Exception {
+        FactoryFinder<TransportFactory> finder = new 
FactoryFinder<>(TRANSPORT_FACTORY_PATH,
+                TransportFactory.class, "");
+        assertEquals(TransportFactory.class, finder.getRequiredType());
+        assertEquals(Set.of(), finder.getAllowedImpls());
+
+        try {
+            // nothing allowed, tcp exists but should be blocked
+            finder.newInstance("tcp");
+            fail("should have failed instantiation");
+        } catch (InstantiationException e) {
+            assertTrue(e.getMessage().contains("is not an allowed 
implementation of type"));
+        }
+    }
+
+    // Test that impls are not filtered if the list is null
+    @Test
+    public void testAllowedImplsFilterAllowAll() throws Exception {
+        // check with constructor that should default to null
+        FactoryFinder<TransportFactory> finder = new 
FactoryFinder<>(TRANSPORT_FACTORY_PATH,
+                TransportFactory.class, null);
+        assertNull(finder.getAllowedImpls());
+        assertNotNull(finder.newInstance("tcp"));
+        assertNotNull(finder.newInstance("ssl"));
+
+        // check again with explicit null
+        finder = new FactoryFinder<>(TRANSPORT_FACTORY_PATH,
+                TransportFactory.class, null);
+        assertNull(finder.getAllowedImpls());
+        assertNotNull(finder.newInstance("tcp"));
+        assertNotNull(finder.newInstance("ssl"));
+
+        try {
+            // abc is allowed because we are not filtering by allowed impls but
+            // we should not be able to find it
+            finder.newInstance("abc");
+            fail("should have failed instantiation");
+        } catch (IOException e) {
+            assertTrue(e.getMessage().contains("Could not find factory class 
for resource"));
+        }
+    }
+
+    @Test
+    public void testAllowedImplsTrimWhiteSpace() throws Exception {
+        // Build CSV with white space between all classes, including two white 
space
+        // only elements
+        String implsWithWhiteSpace = String.join(",   ",
+                TcpTransportFactory.class.getName(), 
SslTransportFactory.class.getName(),
+                " ", NIOTransportFactory.class.getName(), "");
+
+        FactoryFinder<TransportFactory> finder = new 
FactoryFinder<>(TRANSPORT_FACTORY_PATH,
+                TransportFactory.class, implsWithWhiteSpace);
+
+        // all white space should have been trimmed and all empty values 
skipped
+        // leading to the 3 valid classes loaded
+        assertEquals(Set.of(TcpTransportFactory.class, 
SslTransportFactory.class,
+                NIOTransportFactory.class), finder.getAllowedImpls());
+        assertNotNull(finder.newInstance("tcp"));
+        assertNotNull(finder.newInstance("ssl"));
+        assertNotNull(finder.newInstance("nio"));
+    }
+}
diff --git 
a/activemq-jdbc-store/src/main/java/org/apache/activemq/store/jdbc/JDBCPersistenceAdapter.java
 
b/activemq-jdbc-store/src/main/java/org/apache/activemq/store/jdbc/JDBCPersistenceAdapter.java
index eaddfb8340..23f3bc48d4 100644
--- 
a/activemq-jdbc-store/src/main/java/org/apache/activemq/store/jdbc/JDBCPersistenceAdapter.java
+++ 
b/activemq-jdbc-store/src/main/java/org/apache/activemq/store/jdbc/JDBCPersistenceAdapter.java
@@ -75,10 +75,10 @@ import org.slf4j.LoggerFactory;
 public class JDBCPersistenceAdapter extends DataSourceServiceSupport 
implements PersistenceAdapter {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(JDBCPersistenceAdapter.class);
-    private static FactoryFinder adapterFactoryFinder = new FactoryFinder(
-        "META-INF/services/org/apache/activemq/store/jdbc/");
-    private static FactoryFinder lockFactoryFinder = new FactoryFinder(
-        "META-INF/services/org/apache/activemq/store/jdbc/lock/");
+    private static final FactoryFinder<JDBCAdapter> adapterFactoryFinder = new 
FactoryFinder<>(
+        "META-INF/services/org/apache/activemq/store/jdbc/", 
JDBCAdapter.class, null);
+    private static final FactoryFinder<Locker> lockFactoryFinder = new 
FactoryFinder<>(
+        "META-INF/services/org/apache/activemq/store/jdbc/lock/", 
Locker.class, null);
 
     public static final long DEFAULT_LOCK_KEEP_ALIVE_PERIOD = 30 * 1000;
 
@@ -456,7 +456,7 @@ public class JDBCPersistenceAdapter extends 
DataSourceServiceSupport implements
         return adapter;
     }
 
-    private Object loadAdapter(FactoryFinder finder, String kind) throws 
IOException {
+    private Object loadAdapter(FactoryFinder<?> finder, String kind) throws 
IOException {
         Object adapter = null;
         TransactionContext c = getTransactionContext();
         try {
diff --git 
a/activemq-mqtt/src/main/java/org/apache/activemq/transport/mqtt/MQTTProtocolConverter.java
 
b/activemq-mqtt/src/main/java/org/apache/activemq/transport/mqtt/MQTTProtocolConverter.java
index 5e9b6234db..badd991217 100644
--- 
a/activemq-mqtt/src/main/java/org/apache/activemq/transport/mqtt/MQTTProtocolConverter.java
+++ 
b/activemq-mqtt/src/main/java/org/apache/activemq/transport/mqtt/MQTTProtocolConverter.java
@@ -126,7 +126,9 @@ public class MQTTProtocolConverter {
 
     public int version;
 
-    private final FactoryFinder STRATAGY_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/transport/strategies/");
+    private final FactoryFinder<MQTTSubscriptionStrategy> STRATEGY_FINDER =
+            new 
FactoryFinder<>("META-INF/services/org/apache/activemq/transport/strategies/",
+                    MQTTSubscriptionStrategy.class, null);
 
     /*
      * Subscription strategy configuration element.
@@ -861,7 +863,7 @@ public class MQTTProtocolConverter {
 
     protected MQTTSubscriptionStrategy findSubscriptionStrategy() throws 
IOException {
         if (subsciptionStrategy == null) {
-            synchronized (STRATAGY_FINDER) {
+            synchronized (STRATEGY_FINDER) {
                 if (subsciptionStrategy != null) {
                     return subsciptionStrategy;
                 }
@@ -869,7 +871,7 @@ public class MQTTProtocolConverter {
                 MQTTSubscriptionStrategy strategy = null;
                 if (subscriptionStrategyName != null && 
!subscriptionStrategyName.isEmpty()) {
                     try {
-                        strategy = (MQTTSubscriptionStrategy) 
STRATAGY_FINDER.newInstance(subscriptionStrategyName);
+                        strategy = 
STRATEGY_FINDER.newInstance(subscriptionStrategyName);
                         LOG.debug("MQTT Using subscription strategy: {}", 
subscriptionStrategyName);
                         if (strategy instanceof BrokerServiceAware) {
                             
((BrokerServiceAware)strategy).setBrokerService(brokerService);
diff --git a/activemq-spring/pom.xml b/activemq-spring/pom.xml
index ed1a740610..f4b4b059d5 100644
--- a/activemq-spring/pom.xml
+++ b/activemq-spring/pom.xml
@@ -240,7 +240,7 @@
               <schema>${basedir}/target/classes/activemq.xsd</schema>
               <outputDir>${basedir}/target/classes</outputDir>
               <generateSpringSchemasFile>false</generateSpringSchemasFile>
-              
<excludedClasses>org.apache.activemq.broker.jmx.AnnotatedMBean,org.apache.activemq.broker.jmx.DestinationViewMBean</excludedClasses>
+              
<excludedClasses>org.apache.activemq.broker.jmx.AnnotatedMBean,org.apache.activemq.broker.jmx.DestinationViewMBean,org.apache.activemq.util.FactoryFinder</excludedClasses>
             </configuration>
             <goals>
               <goal>mapping</goal>
diff --git 
a/activemq-stomp/src/main/java/org/apache/activemq/transport/stomp/ProtocolConverter.java
 
b/activemq-stomp/src/main/java/org/apache/activemq/transport/stomp/ProtocolConverter.java
index 70456a7cd6..4d96cf9eff 100644
--- 
a/activemq-stomp/src/main/java/org/apache/activemq/transport/stomp/ProtocolConverter.java
+++ 
b/activemq-stomp/src/main/java/org/apache/activemq/transport/stomp/ProtocolConverter.java
@@ -84,6 +84,10 @@ public class ProtocolConverter {
     private static final String BROKER_VERSION;
     private static final StompFrame ping = new 
StompFrame(Stomp.Commands.KEEPALIVE);
 
+    public static final String DEFAULT_ALLOWED_TRANSLATORS = 
FactoryFinder.buildAllowedImpls(
+            JmsFrameTranslator.class, LegacyFrameTranslator.class);
+    public static final String FRAME_TRANSLATOR_CLASSES_PROP = 
"org.apache.activemq.stomp.FRAME_TRANSLATOR_CLASSES";
+
     static {
         String version = "5.6.0";
         try(InputStream in = 
ProtocolConverter.class.getResourceAsStream("/org/apache/activemq/version.txt"))
 {
@@ -108,7 +112,7 @@ public class ProtocolConverter {
     private final LongSequenceGenerator transactionIdGenerator = new 
LongSequenceGenerator();
     private final LongSequenceGenerator tempDestinationGenerator = new 
LongSequenceGenerator();
 
-    private final ConcurrentMap<Integer, ResponseHandler> resposeHandlers = 
new ConcurrentHashMap<>();
+    private final ConcurrentMap<Integer, ResponseHandler> responseHandlers = 
new ConcurrentHashMap<>();
     private final ConcurrentMap<ConsumerId, StompSubscription> 
subscriptionsByConsumerId = new ConcurrentHashMap<>();
     private final ConcurrentMap<String, StompSubscription> subscriptions = new 
ConcurrentHashMap<>();
     private final ConcurrentMap<String, ActiveMQDestination> tempDestinations 
= new ConcurrentHashMap<>();
@@ -121,13 +125,13 @@ public class ProtocolConverter {
     // Read-Only view used in this class to enforce the separation of read vs 
update of the global index.
     private final Map<String, StompAckEntry> pendingAcks = 
Collections.unmodifiableMap(pendingAcksTracker);
 
-    private final Object commnadIdMutex = new Object();
+    private final Object commandIdMutex = new Object();
     private int lastCommandId;
     private final AtomicBoolean connected = new AtomicBoolean(false);
-    private final FrameTranslator frameTranslator = new 
LegacyFrameTranslator();
-    private ConcurrentMap<String, FrameTranslator> jmsFrameTranslators=new 
ConcurrentHashMap<String,FrameTranslator>();
-  
-    private final FactoryFinder FRAME_TRANSLATOR_FINDER = new 
FactoryFinder("META-INF/services/org/apache/activemq/transport/frametranslator/");
+    private final FrameTranslator defaultFrameTranslator = new 
LegacyFrameTranslator();
+    private final Map<String, FrameTranslator> jmsFrameTranslators = new 
ConcurrentHashMap<>();
+
+    private final FactoryFinder<FrameTranslator> frameTranslatorFinder;
     private final BrokerContext brokerContext;
     private String version = "1.0";
     private long hbReadInterval;
@@ -138,10 +142,12 @@ public class ProtocolConverter {
     public ProtocolConverter(StompTransport stompTransport, BrokerContext 
brokerContext) {
         this.stompTransport = stompTransport;
         this.brokerContext = brokerContext;
+        this.frameTranslatorFinder = new 
FactoryFinder<>("META-INF/services/org/apache/activemq/transport/frametranslator/",
+                FrameTranslator.class, 
System.getProperty(FRAME_TRANSLATOR_CLASSES_PROP, DEFAULT_ALLOWED_TRANSLATORS));
     }
 
     protected int generateCommandId() {
-        synchronized (commnadIdMutex) {
+        synchronized (commandIdMutex) {
             return lastCommandId++;
         }
     }
@@ -174,7 +180,7 @@ public class ProtocolConverter {
         command.setCommandId(generateCommandId());
         if (handler != null) {
             command.setResponseRequired(true);
-            resposeHandlers.put(command.getCommandId(), handler);
+            responseHandlers.put(command.getCommandId(), handler);
         }
         stompTransport.sendToActiveMQ(command);
     }
@@ -188,29 +194,21 @@ public class ProtocolConverter {
     }
 
     protected FrameTranslator findTranslator(String header, 
ActiveMQDestination destination, boolean advisory) {
-        FrameTranslator translator = frameTranslator;
+        FrameTranslator translator = null;
         try {
             if (header != null) {
-               translator=jmsFrameTranslators.get(header);
-               if(translator==null) {
-                       LOG.info("Creating a new FrameTranslator to convert 
"+header);
-                       translator = (FrameTranslator) 
FRAME_TRANSLATOR_FINDER.newInstance(header);
-                       if(translator!=null) {
-                               LOG.info("Created a new FrameTranslator to 
convert "+header);
-                               jmsFrameTranslators.put(header,translator);
-                       }else {
-                               LOG.error("Failed in creating FrameTranslator 
to convert "+header);
-                       }                               
-               }
-            } else {
-                if (destination != null && (advisory || 
AdvisorySupport.isAdvisoryTopic(destination))) {
-                    translator = new JmsFrameTranslator();
-                }
+               translator = loadTranslator(header);
+            } else if (destination != null && (advisory || 
AdvisorySupport.isAdvisoryTopic(destination))) {
+                translator = new JmsFrameTranslator();
             }
         } catch (Exception ignore) {
             // if anything goes wrong use the default translator
                        LOG.debug("Failed in getting a FrameTranslator to 
convert ", ignore);
-               translator = frameTranslator;
+        }
+
+        // fallback to default if we can't find/load one
+        if (translator == null) {
+            translator = defaultFrameTranslator;
         }
 
         if (translator instanceof BrokerContextAware) {
@@ -219,6 +217,23 @@ public class ProtocolConverter {
 
         return translator;
     }
+    
+    protected FrameTranslator loadTranslator(String header) throws Exception {
+        FrameTranslator translator = jmsFrameTranslators.get(header);
+
+        if (translator == null) {
+            LOG.info("Creating a new FrameTranslator to convert {}", header);
+            translator = frameTranslatorFinder.newInstance(header);
+            if (translator != null) {
+                LOG.info("Created a new FrameTranslator to convert {}", 
header);
+                jmsFrameTranslators.put(header, translator);
+            } else {
+                LOG.error("Failed in creating FrameTranslator to convert {}", 
header);
+            }
+        }
+        
+        return translator;
+    }
 
     /**
      * Convert a STOMP command
@@ -617,9 +632,11 @@ public class ProtocolConverter {
 
         StompSubscription stompSubscription;
         if (!consumerInfo.isBrowser()) {
-            stompSubscription = new StompSubscription(this, subscriptionId, 
consumerInfo, headers.get(Stomp.Headers.TRANSFORMATION), pendingAcksTracker);
+            stompSubscription = new StompSubscription(this, subscriptionId, 
consumerInfo, headers.get(
+                    Stomp.Headers.TRANSFORMATION), pendingAcksTracker);
         } else {
-            stompSubscription = new StompQueueBrowserSubscription(this, 
subscriptionId, consumerInfo, headers.get(Stomp.Headers.TRANSFORMATION), 
pendingAcksTracker);
+            stompSubscription = new StompQueueBrowserSubscription(this, 
subscriptionId, consumerInfo, headers.get(
+                    Stomp.Headers.TRANSFORMATION), pendingAcksTracker);
         }
         stompSubscription.setDestination(actualDest);
 
@@ -863,7 +880,7 @@ public class ProtocolConverter {
     public void onActiveMQCommand(Command command) throws IOException, 
JMSException {
         if (command.isResponse()) {
             Response response = (Response)command;
-            ResponseHandler rh = 
resposeHandlers.remove(response.getCorrelationId());
+            ResponseHandler rh = 
responseHandlers.remove(response.getCorrelationId());
             if (rh != null) {
                 rh.onResponse(this, response);
             } else {
@@ -895,7 +912,7 @@ public class ProtocolConverter {
 
     public StompFrame convertMessage(ActiveMQMessage message, boolean 
ignoreTransformation) throws IOException, JMSException {
         if (ignoreTransformation == true) {
-            return frameTranslator.convertMessage(this, message);
+            return defaultFrameTranslator.convertMessage(this, message);
         } else {
             FrameTranslator translator = findTranslator(
                 message.getStringProperty(Stomp.Headers.TRANSFORMATION), 
message.getDestination(), message.isAdvisory());
diff --git 
a/activemq-stomp/src/test/java/org/apache/activemq/transport/stomp/ProtocolConverterTest.java
 
b/activemq-stomp/src/test/java/org/apache/activemq/transport/stomp/ProtocolConverterTest.java
new file mode 100644
index 0000000000..17d293e3d8
--- /dev/null
+++ 
b/activemq-stomp/src/test/java/org/apache/activemq/transport/stomp/ProtocolConverterTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.transport.stomp;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+import org.apache.activemq.transport.stomp.StompTest.TestTranslator;
+import org.apache.activemq.util.FactoryFinder;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(ParallelTest.class)
+public class ProtocolConverterTest {
+
+    private final StompTransport transport = mock(StompTransport.class);
+    // Before each test reset to defaults
+    @Before
+    public void setUp() {
+        resetDefaultAllowedTranslators();
+    }
+
+    // clean up after all tests are done finished by resetting the property 
back before exiting
+    @AfterClass
+    public static void tearDown() {
+        resetDefaultAllowedTranslators();
+    }
+
+    @Test
+    public void testLoadMissingTranslator() throws Exception {
+        ProtocolConverter converter = new ProtocolConverter(transport, null);
+
+        // Should fallback to default
+        assertEquals(LegacyFrameTranslator.class, 
converter.findTranslator("abc").getClass());
+
+        try {
+            converter.loadTranslator("abc");
+            fail("should have failed");
+        } catch (Exception e) {
+            assertTrue(e.getMessage().contains("Could not find factory class 
for resource"));
+        }
+    }
+
+    @Test
+    public void testAllowedTranslators() throws Exception {
+        // first test defaults
+        ProtocolConverter converter = new ProtocolConverter(transport, null);
+        assertNotNull(converter.loadTranslator("jms-json"));
+        assertEquals(JmsFrameTranslator.class, 
converter.findTranslator("jms-json").getClass());
+        assertNotNull(converter.loadTranslator("jms-xml"));
+        assertEquals(JmsFrameTranslator.class, 
converter.findTranslator("jms-xml").getClass());
+
+        // test specific allowed
+        System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP,
+                JmsFrameTranslator.class.getName());
+        converter = new ProtocolConverter(transport, null);
+        assertEquals(JmsFrameTranslator.class, 
converter.findTranslator("jms-json").getClass());
+        assertEquals(JmsFrameTranslator.class, 
converter.findTranslator("jms-byte").getClass());
+
+        // test all allowed
+        System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP, 
"*");
+        converter = new ProtocolConverter(transport, null);
+        assertEquals(JmsFrameTranslator.class, 
converter.findTranslator("jms-json").getClass());
+        assertEquals(JmsFrameTranslator.class, 
converter.findTranslator("jms-byte").getClass());
+        // our custom is also allowed
+        assertNotNull(converter.loadTranslator("stomp-test"));
+        assertEquals(TestTranslator.class, 
converter.findTranslator("stomp-test").getClass());
+    }
+
+    @Test
+    public void testAllowedTranslatorsErrors() throws Exception {
+        // Disable all and allow none
+        System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP, 
"");
+        ProtocolConverter converter = new ProtocolConverter(transport, null);
+
+        assertTranslatorNotAllowed(converter, "jms-json");
+
+        // should fall back to default legacy on error
+        assertEquals(LegacyFrameTranslator.class, 
converter.findTranslator("jms-json").getClass());
+
+        // test custom translator not in allowed list
+        System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP, 
JmsFrameTranslator.class.getName());
+        converter = new ProtocolConverter(transport, null);
+        assertTranslatorNotAllowed(converter, "stomp-test");
+
+    }
+
+    @Test
+    public void testCustomAllowedTranslator() throws Exception {
+        System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP,
+                FactoryFinder.buildAllowedImpls(JmsFrameTranslator.class, 
TestTranslator.class));
+        ProtocolConverter converter = new ProtocolConverter(transport, null);
+
+        // JmsFrameTranslator is allowed
+        assertNotNull(converter.loadTranslator("jms-json"));
+        assertEquals(JmsFrameTranslator.class, 
converter.findTranslator("jms-json").getClass());
+
+        // our custom is also allowed
+        assertNotNull(converter.loadTranslator("stomp-test"));
+        assertEquals(TestTranslator.class, 
converter.findTranslator("stomp-test").getClass());
+    }
+
+    @Test
+    public void testWhiteSpaceAllowedTranslator() throws Exception {
+        // white space should be trimmed
+        System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP,
+                String.join(",   ", JmsFrameTranslator.class.getName(),
+                        TestTranslator.class.getName()));
+        ProtocolConverter converter = new ProtocolConverter(transport, null);
+
+        // JmsFrameTranslator is allowed
+        assertNotNull(converter.loadTranslator("jms-json"));
+        assertEquals(JmsFrameTranslator.class, 
converter.findTranslator("jms-json").getClass());
+
+        // our custom is also allowed
+        assertNotNull(converter.loadTranslator("stomp-test"));
+        assertEquals(TestTranslator.class, 
converter.findTranslator("stomp-test").getClass());
+    }
+
+    @Test
+    public void testMissingInterface() throws Exception {
+        System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP, 
"*");
+        ProtocolConverter converter = new ProtocolConverter(transport, null);
+
+        try {
+            converter.loadTranslator("stomp-test-invalid");
+            fail("should have failed");
+        } catch (InstantiationException e) {
+            assertTrue(e.getMessage().contains(BadTranslator.class.getName() + 
" is not assignable to interface "
+                    + FrameTranslator.class.getName()));
+        }
+
+        // fallback
+        assertEquals(LegacyFrameTranslator.class, 
converter.findTranslator("stomp-test-invalid").getClass());
+    }
+
+    @Test
+    public void testPathTraversal() throws Exception {
+        ProtocolConverter converter = new ProtocolConverter(transport, null);
+
+        try {
+            converter.loadTranslator("../stomp-test");
+            fail("should have failed");
+        } catch (InstantiationException e) {
+            assertTrue(e.getMessage().contains("rovided key escapes the 
FactoryFinder configured directory"));
+        }
+
+        // fallback
+        assertEquals(LegacyFrameTranslator.class, 
converter.findTranslator("../stomp-test").getClass());
+    }
+
+    private void assertTranslatorNotAllowed(ProtocolConverter converter, 
String key) throws Exception {
+        try {
+            converter.loadTranslator(key);
+            fail("Should have failed");
+        } catch (InstantiationException e) {
+            assertTrue(e.getMessage().contains("is not an allowed 
implementation of type interface "
+                    + FrameTranslator.class.getName()));
+        }
+        // should fall back to default legacy on error
+        assertEquals(LegacyFrameTranslator.class, 
converter.findTranslator(key).getClass());
+    }
+
+    private static void resetDefaultAllowedTranslators() {
+        System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP,
+                ProtocolConverter.DEFAULT_ALLOWED_TRANSLATORS);
+    }
+
+    // don't implement the correct interface
+    public static class BadTranslator {
+
+    }
+}
diff --git 
a/activemq-stomp/src/test/java/org/apache/activemq/transport/stomp/StompTest.java
 
b/activemq-stomp/src/test/java/org/apache/activemq/transport/stomp/StompTest.java
index 218dda6524..70fa9ed4f8 100644
--- 
a/activemq-stomp/src/test/java/org/apache/activemq/transport/stomp/StompTest.java
+++ 
b/activemq-stomp/src/test/java/org/apache/activemq/transport/stomp/StompTest.java
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import jakarta.jms.Destination;
 import java.io.IOException;
 import java.io.StringReader;
 import java.net.SocketTimeoutException;
@@ -32,6 +33,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -59,6 +61,7 @@ import org.apache.activemq.broker.region.Subscription;
 import org.apache.activemq.broker.region.policy.PolicyEntry;
 import org.apache.activemq.broker.region.policy.PolicyMap;
 import org.apache.activemq.command.ActiveMQDestination;
+import org.apache.activemq.command.ActiveMQMessage;
 import org.apache.activemq.command.ActiveMQQueue;
 import org.apache.activemq.command.ActiveMQTextMessage;
 import org.apache.activemq.util.Wait;
@@ -1241,6 +1244,89 @@ public class StompTest extends StompTestSupport {
         compareFrameXML(frame, xmlObject);
     }
 
+    @Test(timeout = 60000)
+    public void testTransformationSuccess() throws Exception {
+        // transformation is allowed, use default configured transformers
+        assertStompTransformation(null,
+                Stomp.Transformations.JMS_OBJECT_XML.toString(),
+                frame -> assertTrue(frame.contains("<pojo>")));
+
+        // transformation is allowed
+        
assertStompTransformation(ProtocolConverter.DEFAULT_ALLOWED_TRANSLATORS,
+                Stomp.Transformations.JMS_OBJECT_XML.toString(),
+                frame -> assertTrue(frame.contains("<pojo>")));
+
+        // transformation is allowed with wildcard, xml tag should exist
+        assertStompTransformation("*",
+                Stomp.Transformations.JMS_OBJECT_XML.toString(),
+                frame -> assertTrue(frame.contains("<pojo>")));
+
+        // verify TestTranslator was used and appended header
+        assertStompTransformation(TestTranslator.class.getName(),
+                "stomp-test",
+                frame -> 
assertTrue(frame.contains("stomp-test-translator-header")));
+    }
+
+    @Test(timeout = 60000)
+    public void testTransformationFailuresFallbackToDefault() throws Exception 
{
+        // path traversal is not allowed so given translator should fail
+        
assertStompTransformation(ProtocolConverter.DEFAULT_ALLOWED_TRANSLATORS,
+                "../" + Stomp.Transformations.JMS_OBJECT_XML,
+                frame -> assertEquals(-1, frame.indexOf("<pojo>")));
+
+        // xml translator maps to JmsFrameTranslator.class which is not allowed
+        assertStompTransformation(LegacyFrameTranslator.class.getName(),
+                Stomp.Transformations.JMS_OBJECT_XML.toString(),
+                frame -> assertEquals(-1, frame.indexOf("<pojo>")));
+
+        // Allowed translators are set to none so translator should fail
+        assertStompTransformation("", 
Stomp.Transformations.JMS_OBJECT_XML.toString(),
+                frame -> assertEquals(-1, frame.indexOf("<pojo>")));
+
+        // TestTranslator is not allowed so won't have our header is it will 
fall back to
+        // the legacy default translator
+        
assertStompTransformation(ProtocolConverter.DEFAULT_ALLOWED_TRANSLATORS,
+                "stomp-test",
+                frame -> 
assertFalse(frame.contains("stomp-test-translator-header")));
+    }
+
+    private void assertStompTransformation(String allowedTranslators,
+            String transformation, Consumer<String> verifier) throws Exception 
{
+        try {
+            if (allowedTranslators != null) {
+                tearDown();
+                
System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP,
+                        allowedTranslators);
+                setUp();
+            }
+
+            MessageProducer producer = session.createProducer(
+                    new ActiveMQQueue("USERS." + getQueueName()));
+            ObjectMessage message = session.createObjectMessage(
+                    new SamplePojo("Dejan", "Belgrade"));
+            producer.send(message);
+
+            String frame = "CONNECT\n" + "login:system\n" + 
"passcode:manager\n\n" + Stomp.NULL;
+            stompConnection.sendFrame(frame);
+
+            frame = stompConnection.receiveFrame();
+            assertTrue(frame.startsWith("CONNECTED"));
+
+            frame = "SUBSCRIBE\n" + "destination:/queue/USERS." + 
getQueueName() + "\n" + "ack:auto"
+                    + "\n" + "transformation:" + transformation + "\n\n"
+                    + Stomp.NULL;
+            stompConnection.sendFrame(frame);
+            frame = stompConnection.receiveFrame();
+
+            verifier.accept(frame);
+        } finally {
+            tearDown();
+            System.setProperty(ProtocolConverter.FRAME_TRANSLATOR_CLASSES_PROP,
+                    ProtocolConverter.DEFAULT_ALLOWED_TRANSLATORS);
+            setUp();
+        }
+    }
+
     private void compareFrameXML(String frame, String xmlObject) {
 
         String xmlReceived = frame.trim().substring(frame.indexOf("<pojo>"));
@@ -2554,4 +2640,35 @@ public class StompTest extends StompTestSupport {
         Map<String, String> map = (Map<String, String>)xstream.unmarshal(in);
         return map;
     }
+
+    public static class TestTranslator implements FrameTranslator {
+        private final JmsFrameTranslator translator = new JmsFrameTranslator();
+
+        @Override
+        public ActiveMQMessage convertFrame(ProtocolConverter converter, 
StompFrame command)
+                throws JMSException, ProtocolException {
+            return translator.convertFrame(converter, command);
+        }
+
+        @Override
+        public StompFrame convertMessage(ProtocolConverter converter, 
ActiveMQMessage message)
+                throws IOException, JMSException {
+            StompFrame frame = translator.convertMessage(converter, message);
+            // add a header to make it easy to detect if translator was used
+            frame.getHeaders().put("stomp-test-translator-header", 
"header-value");
+            return frame;
+        }
+
+        @Override
+        public String convertDestination(ProtocolConverter converter, 
Destination d) {
+            return translator.convertDestination(converter, d);
+        }
+
+        @Override
+        public ActiveMQDestination convertDestination(ProtocolConverter 
converter, String name,
+                boolean forceFallback) throws ProtocolException {
+            return translator.convertDestination(converter, name, 
forceFallback);
+        }
+    }
+
 }
diff --git 
a/activemq-stomp/src/test/resources/META-INF/services/org/apache/activemq/transport/frametranslator/stomp-test
 
b/activemq-stomp/src/test/resources/META-INF/services/org/apache/activemq/transport/frametranslator/stomp-test
new file mode 100644
index 0000000000..377559fb27
--- /dev/null
+++ 
b/activemq-stomp/src/test/resources/META-INF/services/org/apache/activemq/transport/frametranslator/stomp-test
@@ -0,0 +1,17 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## -----------------------------
+class=org.apache.activemq.transport.stomp.StompTest$TestTranslator
diff --git 
a/activemq-stomp/src/test/resources/META-INF/services/org/apache/activemq/transport/frametranslator/stomp-test-invalid
 
b/activemq-stomp/src/test/resources/META-INF/services/org/apache/activemq/transport/frametranslator/stomp-test-invalid
new file mode 100644
index 0000000000..6a94af9406
--- /dev/null
+++ 
b/activemq-stomp/src/test/resources/META-INF/services/org/apache/activemq/transport/frametranslator/stomp-test-invalid
@@ -0,0 +1,17 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## -----------------------------
+class=org.apache.activemq.transport.stomp.ProtocolConverterTest$BadTranslator
diff --git 
a/activemq-web/src/main/java/org/apache/activemq/web/QueueBrowseServlet.java 
b/activemq-web/src/main/java/org/apache/activemq/web/QueueBrowseServlet.java
index 7215ddd1b5..435ffdc6c8 100644
--- a/activemq-web/src/main/java/org/apache/activemq/web/QueueBrowseServlet.java
+++ b/activemq-web/src/main/java/org/apache/activemq/web/QueueBrowseServlet.java
@@ -35,6 +35,8 @@ import jakarta.servlet.http.HttpServletResponse;
 import org.apache.activemq.util.FactoryFinder;
 import org.apache.activemq.util.IntrospectionSupport;
 import org.apache.activemq.web.view.MessageRenderer;
+import org.apache.activemq.web.view.RssMessageRenderer;
+import org.apache.activemq.web.view.SimpleMessageRenderer;
 import org.apache.activemq.web.view.XmlMessageRenderer;
 
 /**
@@ -46,10 +48,20 @@ import org.apache.activemq.web.view.XmlMessageRenderer;
  * <li>selector - specifies the SQL 92 selector to apply to the queue</li>
  * </ul>
  *
- * 
+ *
  */
 public class QueueBrowseServlet extends HttpServlet {
-    private static FactoryFinder factoryFinder = new 
FactoryFinder("META-INF/services/org/apache/activemq/web/view/");
+
+    public static final String QUEUE_BROWSE_VIEWS_PROP = 
"org.apache.activemq.web.view.QUEUE_BROWSE_CLASSES";
+    public static final String DEFAULT_ALLOWED_VIEWS = 
FactoryFinder.buildAllowedImpls(
+            RssMessageRenderer.class, XmlMessageRenderer.class, 
SimpleMessageRenderer.class);
+
+    private final  FactoryFinder<MessageRenderer> factoryFinder;
+
+    public QueueBrowseServlet() {
+        this.factoryFinder = new 
FactoryFinder<>("META-INF/services/org/apache/activemq/web/view/",
+                MessageRenderer.class, 
System.getProperty(QUEUE_BROWSE_VIEWS_PROP, DEFAULT_ALLOWED_VIEWS));
+    }
 
     // Implementation methods
     // 
-------------------------------------------------------------------------
@@ -96,24 +108,17 @@ public class QueueBrowseServlet extends HttpServlet {
             style = "simple";
         }
         try {
-            return (MessageRenderer) factoryFinder.newInstance(style);
-        }
-        catch (IllegalAccessException e) {
-            throw new NoSuchViewStyleException(style, e);
-        }
-        catch (InstantiationException e) {
-            throw new NoSuchViewStyleException(style, e);
+            return factoryFinder.newInstance(style);
         }
-        catch (ClassNotFoundException e) {
+        catch (IllegalAccessException | InstantiationException | 
ClassNotFoundException e) {
             throw new NoSuchViewStyleException(style, e);
         }
     }
 
-    @SuppressWarnings("unchecked")
     protected void configureRenderer(HttpServletRequest request, 
MessageRenderer renderer) {
-        Map<String, String> properties = new HashMap<String, String>();
+        Map<String, String> properties = new HashMap<>();
         for (Enumeration<String> iter = request.getParameterNames(); 
iter.hasMoreElements();) {
-            String name = (String) iter.nextElement();
+            String name = iter.nextElement();
             properties.put(name, request.getParameter(name));
         }
         IntrospectionSupport.setProperties(renderer, properties);
diff --git 
a/activemq-web/src/test/java/org/apache/activemq/web/QueueBrowseServletTest.java
 
b/activemq-web/src/test/java/org/apache/activemq/web/QueueBrowseServletTest.java
new file mode 100644
index 0000000000..8abea6d40a
--- /dev/null
+++ 
b/activemq-web/src/test/java/org/apache/activemq/web/QueueBrowseServletTest.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.web;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import jakarta.jms.JMSException;
+import jakarta.jms.Message;
+import jakarta.jms.QueueBrowser;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import org.apache.activemq.util.FactoryFinder;
+import org.apache.activemq.web.view.MessageRenderer;
+import org.apache.activemq.web.view.RssMessageRenderer;
+import org.apache.activemq.web.view.SimpleMessageRenderer;
+import org.apache.activemq.web.view.XmlMessageRenderer;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+
+public class QueueBrowseServletTest {
+
+    private final HttpServletRequest request = mock(HttpServletRequest.class);
+
+    // Before each test reset to defaults
+    @Before
+    public void setUp() {
+        resetDefaultAllowedViews();
+    }
+
+    // clean up after all tests are done finished by resetting the property 
back before exiting
+    @AfterClass
+    public static void tearDown() {
+        resetDefaultAllowedViews();
+    }
+
+    @Test
+    public void testDefaultViews() throws Exception {
+        QueueBrowseServlet servlet = new QueueBrowseServlet();
+
+        when(request.getParameter("view")).thenReturn("simple");
+        assertEquals(SimpleMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+
+        when(request.getParameter("view")).thenReturn(null);
+        assertEquals(SimpleMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+
+        when(request.getParameter("view")).thenReturn("xml");
+        assertEquals(XmlMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+
+        when(request.getParameter("view")).thenReturn("rss");
+        assertEquals(RssMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+    }
+
+    @Test
+    public void testPathTraversal() throws Exception {
+        QueueBrowseServlet servlet = new QueueBrowseServlet();
+        // illegal path traversal
+        when(request.getParameter("view")).thenReturn("../../simple");
+        try {
+            servlet.getMessageRenderer(request);
+            fail("Should have thrown an exception");
+        } catch (NoSuchViewStyleException e) {
+            Throwable rootCause = e.getRootCause();
+            assertTrue(rootCause instanceof InstantiationException);
+            // view is in allow list but wrong interface type
+            assertEquals(rootCause.getMessage(), "Provided key escapes the 
FactoryFinder configured directory");
+        }
+    }
+
+    @Test
+    public void testViewAllowlistSimpleOnly() throws Exception {
+        // only allow simple and rss view
+        System.setProperty(QueueBrowseServlet.QUEUE_BROWSE_VIEWS_PROP,
+                FactoryFinder.buildAllowedImpls(SimpleMessageRenderer.class,
+                        RssMessageRenderer.class));
+
+        QueueBrowseServlet servlet = new QueueBrowseServlet();
+
+        // simple/rss are allowed
+        when(request.getParameter("view")).thenReturn("simple");
+        assertEquals(SimpleMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+        when(request.getParameter("view")).thenReturn("rss");
+        assertEquals(RssMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+
+        // XmlMessageRenderer is not in the allow list so this should be 
blocked
+        assertViewAllowListFailure(servlet, "xml");
+    }
+
+    @Test
+    public void testViewAllowlistNone() throws Exception {
+        // Set to none allowed
+        System.setProperty(QueueBrowseServlet.QUEUE_BROWSE_VIEWS_PROP, "");
+        QueueBrowseServlet servlet = new QueueBrowseServlet();
+        assertViewAllowListFailure(servlet, "simple");
+        assertViewAllowListFailure(servlet, "xml");
+    }
+
+    @Test
+    public void testViewAllowAll() throws Exception {
+        // Allow all
+        System.setProperty(QueueBrowseServlet.QUEUE_BROWSE_VIEWS_PROP, "*");
+        QueueBrowseServlet servlet = new QueueBrowseServlet();
+
+        when(request.getParameter("view")).thenReturn("simple");
+        assertEquals(SimpleMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+    }
+
+    @Test
+    public void testMissingInterface() throws Exception {
+        // Add class with wrong interface to allow list
+        System.setProperty(QueueBrowseServlet.QUEUE_BROWSE_VIEWS_PROP, 
InvalidMessageRender.class.getName());
+
+        try {
+            // servlet should fail on creation
+            new QueueBrowseServlet();
+            fail("Should have thrown an exception");
+        } catch (IllegalArgumentException e) {
+            
assertTrue(e.getMessage().contains(InvalidMessageRender.class.getName() +
+                    " is not assignable to interface " + 
MessageRenderer.class.getName()));
+        }
+        when(request.getParameter("view")).thenReturn("invalid-view");
+
+        // set to allow all
+        System.setProperty(QueueBrowseServlet.QUEUE_BROWSE_VIEWS_PROP, "*");
+        QueueBrowseServlet servlet = new QueueBrowseServlet();
+
+        // should fail on lookup with same interface issue
+        try {
+            when(request.getParameter("view")).thenReturn("invalid-view");
+            servlet.getMessageRenderer(request);
+            fail("Should not be allowed");
+        } catch (NoSuchViewStyleException e) {
+            Throwable rootCause = e.getRootCause();
+            assertTrue(rootCause instanceof InstantiationException);
+            // view is in allow list but wrong interface type
+            
assertTrue(rootCause.getMessage().contains(InvalidMessageRender.class.getName() 
+
+                    " is not assignable to interface " + 
MessageRenderer.class.getName()));
+        }
+    }
+
+    @Test
+    public void testViewCustomAllow() throws Exception {
+        // default view settings
+        QueueBrowseServlet servlet = new QueueBrowseServlet();
+        when(request.getParameter("view")).thenReturn("valid-view");
+
+        // custom view is not in the allow list
+        assertViewAllowListFailure(servlet, "valid-view");
+
+        // reset with view added to allow list
+        System.setProperty(QueueBrowseServlet.QUEUE_BROWSE_VIEWS_PROP, 
ValidMessageRender.class.getName());
+        servlet = new QueueBrowseServlet();
+
+        assertEquals(ValidMessageRender.class, 
servlet.getMessageRenderer(request).getClass());
+    }
+
+    @Test
+    public void testViewsWhiteSpace() throws Exception {
+        // add views with extra white space which should get trimmed
+        System.setProperty(QueueBrowseServlet.QUEUE_BROWSE_VIEWS_PROP,
+                String.join(",  ", SimpleMessageRenderer.class.getName(),
+                        RssMessageRenderer.class.getName()));
+
+        QueueBrowseServlet servlet = new QueueBrowseServlet();
+        when(request.getParameter("view")).thenReturn("simple");
+        assertEquals(SimpleMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+        when(request.getParameter("view")).thenReturn("rss");
+        assertEquals(RssMessageRenderer.class, 
servlet.getMessageRenderer(request).getClass());
+    }
+
+    private void assertViewAllowListFailure(QueueBrowseServlet servlet, String 
view) throws Exception {
+        try {
+            when(request.getParameter("view")).thenReturn(view);
+            servlet.getMessageRenderer(request);
+            fail("Should not be allowed");
+        } catch (NoSuchViewStyleException e) {
+            Throwable rootCause = e.getRootCause();
+            assertTrue(rootCause instanceof InstantiationException);
+            assertTrue(rootCause.getMessage().contains("is not an allowed "
+                    + "implementation of type interface " + 
MessageRenderer.class.getName()));
+        }
+    }
+
+    private static void resetDefaultAllowedViews() {
+        System.setProperty(QueueBrowseServlet.QUEUE_BROWSE_VIEWS_PROP,
+                QueueBrowseServlet.DEFAULT_ALLOWED_VIEWS);
+    }
+
+    // Does not implement the right interface
+    public static class InvalidMessageRender {
+
+    }
+
+    public static class ValidMessageRender implements MessageRenderer {
+
+        @Override
+        public void renderMessages(HttpServletRequest request, 
HttpServletResponse response,
+                QueueBrowser browser) throws IOException, JMSException, 
ServletException {
+
+        }
+
+        @Override
+        public void renderMessage(PrintWriter writer, HttpServletRequest 
request,
+                HttpServletResponse response, QueueBrowser browser, Message 
message)
+                throws JMSException, ServletException {
+
+        }
+    }
+}
diff --git 
a/activemq-web/src/test/resources/META-INF/services/org/apache/activemq/web/view/invalid-view
 
b/activemq-web/src/test/resources/META-INF/services/org/apache/activemq/web/view/invalid-view
new file mode 100644
index 0000000000..6338f3286f
--- /dev/null
+++ 
b/activemq-web/src/test/resources/META-INF/services/org/apache/activemq/web/view/invalid-view
@@ -0,0 +1,19 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+## 
+## http://www.apache.org/licenses/LICENSE-2.0
+## 
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+
+# use a class that does not implement MessageRenderer to test validation
+class=org.apache.activemq.web.QueueBrowseServletTest$InvalidMessageRender
diff --git 
a/activemq-web/src/test/resources/META-INF/services/org/apache/activemq/web/view/valid-view
 
b/activemq-web/src/test/resources/META-INF/services/org/apache/activemq/web/view/valid-view
new file mode 100644
index 0000000000..af1e9e360b
--- /dev/null
+++ 
b/activemq-web/src/test/resources/META-INF/services/org/apache/activemq/web/view/valid-view
@@ -0,0 +1,19 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+## 
+## http://www.apache.org/licenses/LICENSE-2.0
+## 
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+
+# use a class that does not implement MessageRenderer to test validation
+class=org.apache.activemq.web.QueueBrowseServletTest$ValidMessageRender


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information, visit: https://activemq.apache.org/contact


Reply via email to