diff --git a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnection.java b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnection.java index cebf7c43bdd..6b4f1513e27 100644 --- a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnection.java +++ b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnection.java @@ -696,6 +696,13 @@ public String getDeserializationAllowList() { return this.factoryReference.getDeserializationAllowList(); } + public String getSerialFilter() { + return this.factoryReference.getSerialFilter(); + } + + public String getSerialFilterClassName() { + return this.factoryReference.getSerialFilterClassName(); + } private static class JMSFailureListener implements SessionFailureListener { diff --git a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnectionFactory.java b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnectionFactory.java index b4fa3316bb2..a2741edb286 100644 --- a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnectionFactory.java +++ b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnectionFactory.java @@ -94,6 +94,10 @@ public class ActiveMQConnectionFactory extends JNDIStorable implements Connectio private String deserializationAllowList; + private String serialFilter; + + private String serialFilterClassName; + private boolean cacheDestinations; // keeping this field for serialization compatibility only. do not use it @@ -209,6 +213,26 @@ public void setDeserializationAllowList(String allowList) { this.deserializationAllowList = allowList; } + @Override + public String getSerialFilter() { + return serialFilter; + } + + @Override + public void setSerialFilter(String serialFilter) { + this.serialFilter = serialFilter; + } + + @Override + public String getSerialFilterClassName() { + return serialFilterClassName; + } + + @Override + public void setSerialFilterClassName(String serialFilterClassName) { + this.serialFilterClassName = serialFilterClassName; + } + @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { String url = in.readUTF(); diff --git a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQObjectMessage.java b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQObjectMessage.java index e658f428080..3b5284f7071 100644 --- a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQObjectMessage.java +++ b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQObjectMessage.java @@ -21,6 +21,7 @@ import javax.jms.ObjectMessage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.ObjectInputFilter; import java.io.ObjectOutputStream; import java.io.Serializable; @@ -145,6 +146,12 @@ public Serializable getObject() throws JMSException { if (allowList != null) { ois.setAllowList(allowList); } + + ObjectInputFilter oif = ObjectInputFilterFactory.getObjectInputFilter(options); + if (oif != null) { + ois.setObjectInputFilter(oif); + } + Serializable object = (Serializable) ois.readObject(); return object; } catch (Exception e) { diff --git a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQSession.java b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQSession.java index 7382b7d0396..59f52d80c17 100644 --- a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQSession.java +++ b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQSession.java @@ -708,6 +708,14 @@ public String getDeserializationAllowList() { return connection.getDeserializationAllowList(); } + public String getSerialFilter() { + return connection.getSerialFilter(); + } + + public String getSerialFilterClassName() { + return connection.getSerialFilterClassName(); + } + enum ConsumerDurability { DURABLE, NON_DURABLE; } diff --git a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ConnectionFactoryOptions.java b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ConnectionFactoryOptions.java index bf90387f36d..27757a30777 100644 --- a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ConnectionFactoryOptions.java +++ b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ConnectionFactoryOptions.java @@ -42,4 +42,11 @@ public interface ConnectionFactoryOptions { void setDeserializationAllowList(String allowList); + String getSerialFilter(); + + void setSerialFilter(String serialFilter); + + String getSerialFilterClassName(); + + void setSerialFilterClassName(String serialFilterClassName); } diff --git a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ObjectInputFilterFactory.java b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ObjectInputFilterFactory.java new file mode 100644 index 00000000000..fd46e501f29 --- /dev/null +++ b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ObjectInputFilterFactory.java @@ -0,0 +1,143 @@ +/* + * 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.artemis.jms.client; + +import java.io.ObjectInputFilter; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class ObjectInputFilterFactory { + + public static final String SERIAL_FILTER_PROPERTY = "org.apache.activemq.artemis.jms.serialFilter"; + public static final String SERIAL_FILTER_CLASS_NAME_PROPERTY = "org.apache.activemq.artemis.jms.serialFilterClassName"; + + private static Map filterCache = new ConcurrentHashMap<>(); + + public static ObjectInputFilter getObjectInputFilter(ConnectionFactoryOptions options) { + String className = getFilterClassName(options); + if (className != null) { + return getObjectInputFilterForClassName(className); + } + + String pattern = getFilterPattern(options); + if (pattern != null) { + return getObjectInputFilterForPattern(pattern); + } + + return null; + } + + public static ObjectInputFilter getObjectInputFilterForPattern(String pattern) { + if (pattern == null) { + return null; + } + + return filterCache.computeIfAbsent(new PatternKey(pattern), + k -> ObjectInputFilter.Config.createFilter(pattern)); + } + + public static ObjectInputFilter getObjectInputFilterForClassName(String className) { + if (className == null) { + return null; + } + + return filterCache.computeIfAbsent(new ClassNameKey(className), k -> { + try { + return (ObjectInputFilter)Class.forName(className).newInstance(); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Class " + className + " not found.", e); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + private static String getFilterClassName(ConnectionFactoryOptions options) { + if (options != null && options.getSerialFilterClassName() != null) { + return options.getSerialFilterClassName(); + } + + return System.getProperty(SERIAL_FILTER_CLASS_NAME_PROPERTY); + } + + private static String getFilterPattern(ConnectionFactoryOptions options) { + if (options != null && options.getSerialFilter() != null) { + return options.getSerialFilter(); + } + + return System.getProperty(SERIAL_FILTER_PROPERTY); + } + + private static class PatternKey { + private String pattern; + + PatternKey(String pattern) { + this.pattern = pattern; + } + + public String getPattern() { + return pattern; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PatternKey that = (PatternKey) o; + + if (!Objects.equals(pattern, that.pattern)) return false; + + return true; + } + + @Override + public int hashCode() { + return pattern != null ? pattern.hashCode() : 0; + } + } + + private static class ClassNameKey { + private String className; + + ClassNameKey(String className) { + this.className = className; + } + + public String getClassName() { + return className; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClassNameKey that = (ClassNameKey) o; + + if (!Objects.equals(className, that.className)) return false; + + return true; + } + + @Override + public int hashCode() { + return className != null ? className.hashCode() : 0; + } + } +} diff --git a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/config/ConnectionFactoryConfiguration.java b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/config/ConnectionFactoryConfiguration.java index cfc1752bf68..271d85231e8 100644 --- a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/config/ConnectionFactoryConfiguration.java +++ b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/config/ConnectionFactoryConfiguration.java @@ -204,6 +204,14 @@ public interface ConnectionFactoryConfiguration extends EncodingSupport { void setDeserializationAllowList(String allowList); + String getSerialFilter(); + + void setSerialFilter(String serialFilter); + + String getSerialFilterClassName(); + + void setSerialFilterClassName(String serialFilterClassName); + int getInitialMessagePacketSize(); ConnectionFactoryConfiguration setInitialMessagePacketSize(int size); diff --git a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/config/impl/ConnectionFactoryConfigurationImpl.java b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/config/impl/ConnectionFactoryConfigurationImpl.java index 38c549b1787..f19b79c522a 100644 --- a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/config/impl/ConnectionFactoryConfigurationImpl.java +++ b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/config/impl/ConnectionFactoryConfigurationImpl.java @@ -121,6 +121,10 @@ public class ConnectionFactoryConfigurationImpl implements ConnectionFactoryConf private String deserializationAllowList; + private String serialFilter; + + private String serialFilterClassName; + private int initialMessagePacketSize = ActiveMQClient.DEFAULT_INITIAL_MESSAGE_PACKET_SIZE; private boolean enable1xPrefixes = ActiveMQJMSClient.DEFAULT_ENABLE_1X_PREFIXES; @@ -658,6 +662,9 @@ public void decode(final ActiveMQBuffer buffer) { compressionLevel = buffer.readableBytes() > 0 ? BufferHelper.readNullableInteger(buffer) : ActiveMQClient.DEFAULT_COMPRESSION_LEVEL; + serialFilter = buffer.readableBytes() > 0 ? BufferHelper.readNullableSimpleStringAsString(buffer) : null; + + serialFilterClassName = buffer.readableBytes() > 0 ? BufferHelper.readNullableSimpleStringAsString(buffer) : null; } @Override @@ -756,6 +763,10 @@ public void encode(final ActiveMQBuffer buffer) { BufferHelper.writeNullableBoolean(buffer, useTopologyForLoadBalancing); BufferHelper.writeNullableInteger(buffer, compressionLevel); + + BufferHelper.writeAsNullableSimpleString(buffer, serialFilter); + + BufferHelper.writeAsNullableSimpleString(buffer, serialFilterClassName); } @Override @@ -878,7 +889,11 @@ public int getEncodeSize() { BufferHelper.sizeOfNullableBoolean(useTopologyForLoadBalancing) + - BufferHelper.sizeOfNullableInteger(compressionLevel); + BufferHelper.sizeOfNullableInteger(compressionLevel) + + + BufferHelper.sizeOfNullableSimpleString(serialFilter) + + + BufferHelper.sizeOfNullableSimpleString(serialFilterClassName); return size; } @@ -934,6 +949,26 @@ public void setDeserializationAllowList(String allowList) { this.deserializationAllowList = allowList; } + @Override + public String getSerialFilter() { + return serialFilter; + } + + @Override + public void setSerialFilter(String serialFilter) { + this.serialFilter = serialFilter; + } + + @Override + public String getSerialFilterClassName() { + return serialFilterClassName; + } + + @Override + public void setSerialFilterClassName(String serialFilterClassName) { + this.serialFilterClassName = serialFilterClassName; + } + @Override public ConnectionFactoryConfiguration setCompressLargeMessages(boolean compressLargeMessage) { this.compressLargeMessage = compressLargeMessage; diff --git a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/impl/JMSServerManagerImpl.java b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/impl/JMSServerManagerImpl.java index d63d1ca3269..4456097a931 100644 --- a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/impl/JMSServerManagerImpl.java +++ b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/server/impl/JMSServerManagerImpl.java @@ -1219,6 +1219,8 @@ protected ActiveMQConnectionFactory internalCreateCFPOJO(final ConnectionFactory cf.setProtocolManagerFactoryStr(cfConfig.getProtocolManagerFactoryStr()); cf.setDeserializationDenyList(cfConfig.getDeserializationDenyList()); cf.setDeserializationAllowList(cfConfig.getDeserializationAllowList()); + cf.setSerialFilter(cfConfig.getSerialFilter()); + cf.setSerialFilterClassName(cfConfig.getSerialFilterClassName()); cf.setInitialMessagePacketSize(cfConfig.getInitialMessagePacketSize()); cf.setEnable1xPrefixes(cfConfig.isEnable1xPrefixes()); cf.setEnableSharedClientID(cfConfig.isEnableSharedClientID()); diff --git a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java index 637c69c19b6..a7f60352efb 100644 --- a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java +++ b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java @@ -937,6 +937,30 @@ public void setDeserializationAllowList(String deserializationAllowList) { raProperties.setDeserializationAllowList(deserializationAllowList); } + public String getSerialFilter() { + logger.trace("getSerialFilter()"); + + return raProperties.getSerialFilter(); + } + + public void setSerialFilter(String serialFilter) { + logger.trace("setSerialFilter({})", serialFilter); + + raProperties.setSerialFilter(serialFilter); + } + + public String getSerialFilterClassName() { + logger.trace("getSerialFilterClassName()"); + + return raProperties.getSerialFilterClassName(); + } + + public void setSerialFilterClassName(String serialFilterClassName) { + logger.trace("setSerialFilterClassName({})", serialFilterClassName); + + raProperties.setSerialFilterClassName(serialFilterClassName); + } + /** * Get min large message size * @@ -1944,6 +1968,14 @@ private void setParams(final ActiveMQConnectionFactory cf, final ConnectionFacto if (stringVal != null) { cf.setDeserializationAllowList(stringVal); } + stringVal = overrideProperties.getSerialFilter() != null ? overrideProperties.getSerialFilter() : raProperties.getSerialFilter(); + if (stringVal != null) { + cf.setSerialFilter(stringVal); + } + stringVal = overrideProperties.getSerialFilterClassName() != null ? overrideProperties.getSerialFilterClassName() : raProperties.getSerialFilterClassName(); + if (stringVal != null) { + cf.setSerialFilterClassName(stringVal); + } cf.setIgnoreJTA(isIgnoreJTA()); } diff --git a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ConnectionFactoryProperties.java b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ConnectionFactoryProperties.java index 61f9f4a9fe9..8c3a3692bb3 100644 --- a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ConnectionFactoryProperties.java +++ b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ConnectionFactoryProperties.java @@ -130,6 +130,10 @@ public class ConnectionFactoryProperties implements ConnectionFactoryOptions { private String deserializationAllowList; + private String serialFilter; + + private String serialFilterClassName; + private Boolean enableSharedClientID; /** @@ -724,6 +728,28 @@ public void setDeserializationAllowList(String deserializationAllowList) { hasBeenUpdated = true; } + @Override + public String getSerialFilter() { + return serialFilter; + } + + @Override + public void setSerialFilter(String serialFilter) { + this.serialFilter = serialFilter; + hasBeenUpdated = true; + } + + @Override + public String getSerialFilterClassName() { + return serialFilterClassName; + } + + @Override + public void setSerialFilterClassName(String serialFilterClassName) { + this.serialFilterClassName = serialFilterClassName; + hasBeenUpdated = true; + } + public boolean isHasBeenUpdated() { return hasBeenUpdated; } @@ -971,6 +997,18 @@ public boolean equals(Object obj) { } else if (!deserializationAllowList.equals(other.deserializationAllowList)) return false; + if (serialFilter == null) { + if (other.serialFilter != null) + return false; + } else if (!serialFilter.equals(other.serialFilter)) + return false; + + if (serialFilterClassName == null) { + if (other.serialFilterClassName != null) + return false; + } else if (!serialFilterClassName.equals(other.serialFilterClassName)) + return false; + if (this.enable1xPrefixes == null) { if (other.enable1xPrefixes != null) return false; @@ -1034,6 +1072,8 @@ public int hashCode() { result = prime * result + ((connectionParameters == null) ? 0 : connectionParameters.hashCode()); result = prime * result + ((deserializationDenyList == null) ? 0 : deserializationDenyList.hashCode()); result = prime * result + ((deserializationAllowList == null) ? 0 : deserializationAllowList.hashCode()); + result = prime * result + ((serialFilter == null) ? 0 : serialFilter.hashCode()); + result = prime * result + ((serialFilterClassName == null) ? 0 : serialFilterClassName.hashCode()); result = prime * result + ((enable1xPrefixes == null) ? 0 : enable1xPrefixes.hashCode()); result = prime * result + ((enableSharedClientID == null) ? 0 : enableSharedClientID.hashCode()); return result; diff --git a/docs/user-manual/security.adoc b/docs/user-manual/security.adoc index ec6348bc5e9..dff6137b3be 100644 --- a/docs/user-manual/security.adoc +++ b/docs/user-manual/security.adoc @@ -1431,6 +1431,16 @@ comma separated values for allow list These properties, once specified, are eventually set on the corresponding internal factories. +=== Filtering using built-in JVM support + +Now that Apache ActiveMQ Artemis requires a minimum JVM version of 11, built-in Java serialization filtering mechanisms can be utilized. +Instead of providing an `allow list` or `deny list`, you can specify either a `serialFilter` or `serialFilterClassName`. + +* `serialFilter` - A pattern based filter that allows you to define allow/deny lists and constraints limiting graph complexity and size. https://docs.oracle.com/en/java/javase/17/core/serialization-filtering1.html#JSCOR-GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66[Filter Syntax] +* `serialFilterClassName` - For those who need a custom filtering solution, you can supply an implementation of https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/ObjectInputFilter.html[ObjectInputFilter] + +Configure the same way via url, system properties, resource adapter, or set directly on `ActiveMQConnectionFactory` + == Masking Passwords For details about masking passwords in broker.xml please see the xref:masking-passwords.adoc#masking-passwords[Masking Passwords] chapter. diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/ActiveMQConnectionFactoryTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/ActiveMQConnectionFactoryTest.java index d1ad28ff371..8fd57ff2674 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/ActiveMQConnectionFactoryTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/ActiveMQConnectionFactoryTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.fail; import javax.jms.Connection; @@ -32,7 +33,9 @@ import javax.jms.Session; import javax.naming.Context; import javax.naming.InitialContext; +import java.io.ObjectInputFilter; import java.io.Serializable; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.Hashtable; @@ -54,6 +57,7 @@ import org.apache.activemq.artemis.core.server.ActiveMQServers; import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; import org.apache.activemq.artemis.tests.integration.jms.serializables.TestClass1; +import org.apache.activemq.artemis.tests.integration.jms.serializables.TestClass2; import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; import org.apache.activemq.artemis.utils.ObjectInputStreamWithClassLoader; import org.apache.activemq.artemis.utils.RandomUtil; @@ -432,6 +436,154 @@ private Object receiveObjectMessage(String denyList, } } + @Test + public void testSerialFilterPattern() throws Exception { + testSerialFilterPattern(false, false); + } + + @Test + public void testSerialFilterPatternJndi() throws Exception { + testSerialFilterPattern(true, false); + } + + @Test + public void testSerialFilterPatternBrowser() throws Exception { + testSerialFilterPattern(false, true); + } + + @Test + public void testSerialFilterPatternJndiBrowser() throws Exception { + testSerialFilterPattern(true, true); + } + + private void testSerialFilterPattern(boolean useJndi, boolean useBrowser) throws Exception { + String qname = "SerialTestQueue"; + liveService.createQueue(QueueConfiguration.of(qname).setRoutingType(RoutingType.ANYCAST)); + + // default ok + String serialFilter = null; + Object obj = receiveObjectMessageSerialFilter(serialFilter, null, qname, new TestClass1(), useJndi, useBrowser); + assertTrue(obj instanceof TestClass1, "Object is " + obj); + + // allow TestClass1, deny others + serialFilter = "org.apache.activemq.artemis.tests.integration.jms.serializables.TestClass1;!*"; + + obj = receiveObjectMessageSerialFilter(serialFilter, null, qname, new TestClass1(), useJndi, useBrowser); + assertTrue(obj instanceof TestClass1, "Object is " + obj); + + obj = receiveObjectMessageSerialFilter(serialFilter, null, qname, new TestClass2(), useJndi, useBrowser); + assertTrue(obj instanceof JMSException, "Object is " + obj); + + + // deny TestClass1, allow others + serialFilter = "!org.apache.activemq.artemis.tests.integration.jms.serializables.TestClass1;*"; + + obj = receiveObjectMessageSerialFilter(serialFilter, null, qname, new TestClass1(), useJndi, useBrowser); + assertTrue(obj instanceof JMSException, "Object is " + obj); + + obj = receiveObjectMessageSerialFilter(serialFilter, null, qname, new TestClass2(), useJndi, useBrowser); + assertTrue(obj instanceof TestClass2, "Object is " + obj); + } + + @Test + public void testSerialFilterClass() throws Exception { + testSerialFilterClass(false, false); + } + + @Test + public void testSerialFilterClassJndi() throws Exception { + testSerialFilterClass(true, false); + } + + @Test + public void testSerialFilterClassBrowser() throws Exception { + testSerialFilterClass(false, true); + } + + @Test + public void testSerialFilterClassJndiBrowser() throws Exception { + testSerialFilterClass(true, true); + } + + private void testSerialFilterClass(boolean useJndi, boolean useBrowser) throws Exception { + String qname = "SerialTestQueue"; + liveService.createQueue(QueueConfiguration.of(qname).setRoutingType(RoutingType.ANYCAST)); + + // default ok + String serialClassName = null; + Object obj = receiveObjectMessageSerialFilter(null, serialClassName, qname, new TestClass1(), useJndi, useBrowser); + assertTrue(obj instanceof TestClass1, "Object is " + obj); + + // allow all + serialClassName = AlwaysAcceptObjectInputFilter.class.getName(); + obj = receiveObjectMessageSerialFilter(null, serialClassName, qname, new TestClass1(), useJndi, useBrowser); + assertTrue(obj instanceof TestClass1, "Object is " + obj); + + // reject all + serialClassName = AlwaysRejectObjectInputFilter.class.getName(); + obj = receiveObjectMessageSerialFilter(null, serialClassName, qname, new TestClass1(), useJndi, useBrowser); + assertTrue(obj instanceof JMSException, "Object is " + obj); + } + + private Object receiveObjectMessageSerialFilter(String filterPattern, + String filterClassName, + String qname, + Serializable obj, + boolean useJndi, + boolean useBrowser) throws Exception { + + assertFalse(filterPattern != null && filterClassName != null); // Only supply pattern or classname + + sendObjectMessage(qname, obj); + + StringBuilder query = new StringBuilder(""); + if (filterPattern != null) { + query.append("?"); + query.append("serialFilter="); + query.append(URLEncoder.encode(filterPattern)); + } + + if (filterClassName != null) { + query.append("?"); + query.append("serialFilterClassName="); + query.append(URLEncoder.encode(filterClassName)); + } + + ActiveMQConnectionFactory factory = null; + if (useJndi) { + Hashtable props = new Hashtable<>(); + props.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.activemq.artemis.jndi.ActiveMQInitialContextFactory"); + props.put("connectionFactory.VmConnectionFactory", "vm://0" + query); + Context ctx = new InitialContext(props); + factory = (ActiveMQConnectionFactory) ctx.lookup("VmConnectionFactory"); + } else { + factory = new ActiveMQConnectionFactory("vm://0" + query); + } + + try (Connection connection = factory.createConnection()) { + connection.start(); + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + Queue queue = session.createQueue(qname); + Object result = null; + if (useBrowser) { + QueueBrowser browser = session.createBrowser(queue); + ObjectMessage objMessage = (ObjectMessage) browser.getEnumeration().nextElement(); + //drain message before triggering deserialization + MessageConsumer consumer = session.createConsumer(queue); + consumer.receive(5000); + result = objMessage.getObject(); + } else { + MessageConsumer consumer = session.createConsumer(queue); + ObjectMessage objMessage = (ObjectMessage) consumer.receive(5000); + assertNotNull(objMessage); + result = objMessage.getObject(); + } + return result; + } catch (Exception e) { + return e; + } + } + private void sendObjectMessage(String qname, Serializable obj) throws Exception { ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("vm://0"); Connection connection = factory.createConnection(); @@ -738,4 +890,17 @@ private void startServer() throws Exception { liveService.start(); } + public static class AlwaysRejectObjectInputFilter implements ObjectInputFilter { + @Override + public Status checkInput(FilterInfo filterInfo) { + return Status.REJECTED; + } + } + + public static class AlwaysAcceptObjectInputFilter implements ObjectInputFilter { + @Override + public Status checkInput(FilterInfo filterInfo) { + return Status.ALLOWED; + } + } } diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/serializables/TestClass2.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/serializables/TestClass2.java new file mode 100644 index 00000000000..c8b5389e6e6 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/serializables/TestClass2.java @@ -0,0 +1,23 @@ +/* + * 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.artemis.tests.integration.jms.serializables; + +import java.io.Serializable; + +public class TestClass2 implements Serializable { + +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ActiveMQMessageHandlerTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ActiveMQMessageHandlerTest.java index 12fc2703670..31f2931a882 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ActiveMQMessageHandlerTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ActiveMQMessageHandlerTest.java @@ -32,6 +32,7 @@ import javax.jms.Session; import javax.resource.ResourceException; import javax.resource.spi.InvalidPropertyException; +import java.io.ObjectInputFilter; import java.io.Serializable; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; @@ -213,9 +214,70 @@ public void testObjectMessageReceiveSerializationControl5() throws Exception { } private void testDeserialization(String denyList, String allowList, boolean shouldSucceed) throws Exception { + testDeserialization(denyList, allowList, null, null, shouldSucceed); + } + + @Test + public void testObjectMessageReceiveSerializationControlSF() throws Exception { + String serialFilter = "!org.apache.activemq.artemis.tests.integration.ra.**;*"; + testDeserializationSerialFilter(serialFilter, false); + } + + @Test + public void testObjectMessageReceiveSerializationControl1SF() throws Exception { + String serialFilter = "!some.other.pkg.**;org.apache.activemq.artemis.tests.integration.ra.*"; + testDeserializationSerialFilter(serialFilter, true); + } + + @Test + public void testObjectMessageReceiveSerializationControl2SF() throws Exception { + String serialFilter = "!*;org.apache.activemq.artemis.tests.integration.ra.**"; + testDeserializationSerialFilter(serialFilter, false); + } + + @Test + public void testObjectMessageReceiveSerializationControl3SF() throws Exception { + String serialFilter = "!org.apache.activemq.artemis.tests.**;" + + "org.apache.activemq.artemis.tests.integration.ra.**"; + testDeserializationSerialFilter(serialFilter, false); + } + + @Test + public void testObjectMessageReceiveSerializationControl4SF() throws Exception { + String serialFilter = "some.other.pkg.**;!*"; + testDeserializationSerialFilter(serialFilter, false); + } + + @Test + public void testObjectMessageReceiveSerializationControl5SF() throws Exception { + String serialFilter = null; + testDeserializationSerialFilter(serialFilter, true); + } + + private void testDeserializationSerialFilter(String serialFilter, boolean shouldSucceed) throws Exception { + testDeserialization(null, null, serialFilter, null, shouldSucceed); + } + + @Test + public void testObjectMessageReceiveSerializationControlSFC1() throws Exception { + testDeserializationSerialFilterClassName(AlwaysRejectObjectInputFilter.class.getName(), false); + } + + @Test + public void testObjectMessageReceiveSerializationControlSFC2() throws Exception { + testDeserializationSerialFilterClassName(AlwaysAcceptObjectInputFilter.class.getName(), true); + } + + private void testDeserializationSerialFilterClassName(String serialFilterClassName, boolean shouldSucceed) throws Exception { + testDeserialization(null, null, null, serialFilterClassName, shouldSucceed); + } + + private void testDeserialization(String denyList, String allowList, String serialFilter, String serialFilterClassName, boolean shouldSucceed) throws Exception { ActiveMQResourceAdapter qResourceAdapter = newResourceAdapter(); qResourceAdapter.setDeserializationDenyList(denyList); qResourceAdapter.setDeserializationAllowList(allowList); + qResourceAdapter.setSerialFilter(serialFilter); + qResourceAdapter.setSerialFilterClassName(serialFilterClassName); MyBootstrapContext ctx = new MyBootstrapContext(); qResourceAdapter.start(ctx); @@ -1049,4 +1111,18 @@ public void onMessage(Message message) { static class DummySerializable implements Serializable { } + + public static class AlwaysRejectObjectInputFilter implements ObjectInputFilter { + @Override + public Status checkInput(FilterInfo filterInfo) { + return Status.REJECTED; + } + } + + public static class AlwaysAcceptObjectInputFilter implements ObjectInputFilter { + @Override + public Status checkInput(FilterInfo filterInfo) { + return Status.ALLOWED; + } + } } diff --git a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/ra/ActiveMQResourceAdapterConfigTest.java b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/ra/ActiveMQResourceAdapterConfigTest.java index bafbdd91d92..fe202ac9420 100644 --- a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/ra/ActiveMQResourceAdapterConfigTest.java +++ b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/ra/ActiveMQResourceAdapterConfigTest.java @@ -436,6 +436,18 @@ public class ActiveMQResourceAdapterConfigTest extends ActiveMQTestBase { " " + " " + " " + + " Serial filter pattern to match or reject classes during deserialization" + + " SerialFilter" + + " java.lang.String" + + " " + + " " + + " " + + " Class name of an ObjectInputFilter to use during deserialization" + + " SerialFilterClassName" + + " java.lang.String" + + " " + + " " + + " " + " ***add***" + " IgnoreJTA" + " boolean" + diff --git a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/ra/ResourceAdapterTest.java b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/ra/ResourceAdapterTest.java index 8959e0250b8..75cd2f83aae 100644 --- a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/ra/ResourceAdapterTest.java +++ b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/ra/ResourceAdapterTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.fail; import javax.jms.Connection; +import java.io.ObjectInputFilter; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -604,6 +605,70 @@ public void testActivationDeserializationParameters() throws Exception { } } + @Test + public void testActivationDeserializationParameters2() throws Exception { + ActiveMQServer server = createServer(false); + + try { + + server.start(); + + ActiveMQResourceAdapter ra = new ActiveMQResourceAdapter(); + + ra.setConnectorClassName(INVM_CONNECTOR_FACTORY); + ra.setUserName("userGlobal"); + ra.setPassword("passwordGlobal"); + ra.setSerialFilter("a.b.c.d.e.**;!f.g.h.i.j.**"); + ra.start(new BootstrapContext()); + + ActiveMQConnectionFactory factory = ra.getDefaultActiveMQConnectionFactory(); + assertEquals("a.b.c.d.e.**;!f.g.h.i.j.**", factory.getSerialFilter()); + + ConnectionFactoryProperties overrides = new ConnectionFactoryProperties(); + overrides.setSerialFilter("k.l.m.n.**;!o.p.q.r.**"); + + factory = ra.newConnectionFactory(overrides); + assertEquals("k.l.m.n.**;!o.p.q.r.**", factory.getSerialFilter()); + + ra.stop(); + + } finally { + server.stop(); + } + } + + @Test + public void testActivationDeserializationParameters3() throws Exception { + ActiveMQServer server = createServer(false); + + try { + + server.start(); + + ActiveMQResourceAdapter ra = new ActiveMQResourceAdapter(); + + ra.setConnectorClassName(INVM_CONNECTOR_FACTORY); + ra.setUserName("userGlobal"); + ra.setPassword("passwordGlobal"); + ra.setSerialFilterClassName(AlwaysRejectObjectInputFilter.class.getName()); + ra.start(new BootstrapContext()); + + ActiveMQConnectionFactory factory = ra.getDefaultActiveMQConnectionFactory(); + assertEquals(AlwaysRejectObjectInputFilter.class.getName(), factory.getSerialFilterClassName()); + + ConnectionFactoryProperties overrides = new ConnectionFactoryProperties(); + overrides.setSerialFilterClassName(AlwaysAcceptObjectInputFilter.class.getName()); + + factory = ra.newConnectionFactory(overrides); + assertEquals(AlwaysAcceptObjectInputFilter.class.getName(), factory.getSerialFilterClassName()); + + ra.stop(); + + } finally { + server.stop(); + } + } + @Test public void testForConnectionLeakDuringActivationWhenSessionCreationFails() throws Exception { ActiveMQServer server = createServer(false); @@ -654,4 +719,18 @@ public void testForConnectionLeakDuringActivationWhenSessionCreationFails() thro server.stop(); } } + + public static class AlwaysRejectObjectInputFilter implements ObjectInputFilter { + @Override + public Status checkInput(FilterInfo filterInfo) { + return Status.REJECTED; + } + } + + public static class AlwaysAcceptObjectInputFilter implements ObjectInputFilter { + @Override + public Status checkInput(FilterInfo filterInfo) { + return Status.ALLOWED; + } + } } diff --git a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/util/ObjectInputStreamWithClassLoaderTest.java b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/util/ObjectInputStreamWithClassLoaderTest.java index 59956210424..231aa97261d 100644 --- a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/util/ObjectInputStreamWithClassLoaderTest.java +++ b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/util/ObjectInputStreamWithClassLoaderTest.java @@ -28,6 +28,8 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InvalidClassException; +import java.io.ObjectInputFilter; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.InvocationHandler; @@ -45,6 +47,9 @@ import java.util.Set; import java.util.StringTokenizer; +import org.apache.activemq.artemis.jms.client.ConnectionFactoryOptions; +import org.apache.activemq.artemis.jms.client.ObjectInputFilterFactory; +import org.apache.activemq.artemis.ra.ConnectionFactoryProperties; import org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.EnclosingClass; import org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1; import org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass2; @@ -52,6 +57,9 @@ import org.apache.activemq.artemis.tests.util.ArtemisTestCase; import org.apache.activemq.artemis.utils.ObjectInputStreamWithClassLoader; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class ObjectInputStreamWithClassLoaderTest extends ActiveMQTestBase { @@ -477,6 +485,379 @@ public void testAllowDenyListSystemProperty() throws Exception { } } + // serialFilter version of testAllowDenyList() + @Test + public void testSerialFilter() throws Exception { + File serializeFile = new File(temporaryFolder, "testclass.bin"); + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(serializeFile)); + try { + outputStream.writeObject(new TestClass1()); + outputStream.flush(); + } finally { + outputStream.close(); + } + + String serialFilter; + Exception result; + + // default + assertNull(readSerializedObjectSF(null, serializeFile)); + + // Allow all + assertNull(readSerializedObjectSF("*", serializeFile)); + + // Reject all + result = readSerializedObjectSF("!*", serializeFile); + assertTrue(result instanceof InvalidClassException); + + // allow list + + // sub-package + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.**;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // package + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + + // other package + serialFilter = "some.other.package.*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // deny list + + // sub-package + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.**"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // package + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // other package + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg2.*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + + + serialFilter = "!some.other.package.*;some.other.package1.*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + + // deny list priority + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.*;!some.other.package.*;" + + "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.*;*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.pkg2.*;!some.other.package.*;" + + "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + + serialFilter = "!some.other.package.*;!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg2.*;" + + "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + + + // wildcard + serialFilter = "!*;org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + serialFilter = "!*;*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + serialFilter = "*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + } + + // serialFilter version of testAllowDenyListAgainstArrayObject() + @Test + public void testSerialFilterAgainstArrayObject() throws Exception { + File serializeFile = new File(temporaryFolder, "testclass.bin"); + TestClass1[] sourceObject = new TestClass1[]{new TestClass1()}; + + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(serializeFile)); + try { + outputStream.writeObject(sourceObject); + outputStream.flush(); + } finally { + outputStream.close(); + } + + String serialFilter; + Exception result; + + // default + assertNull(readSerializedObjectSF(null, serializeFile)); + + // now deny TestClass1 + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // now allow TestClass1, it should pass. + + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + } + + // serialFilter version of testAllowDenyListAgainstListObject() + @Test + public void testSerialFilterListAgainstListObject() throws Exception { + File serializeFile = new File(temporaryFolder, "testclass.bin"); + List sourceObject = new ArrayList<>(); + sourceObject.add(new TestClass1()); + + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(serializeFile)); + try { + outputStream.writeObject(sourceObject); + outputStream.flush(); + } finally { + outputStream.close(); + } + + String serialFilter; + Exception result; + + // default + assertNull(readSerializedObjectSF(null, serializeFile)); + + // now deny TestClass1 + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // now allow TestClass1, should fail because the List type is not allowed + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // now add List to allow list, it should pass + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1;java.util.ArrayList;java.lang.Object;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + } + + // serialFilter version of testAllowDenyListAgainstListMapObject() + @Test + public void testSerialFilterAgainstListMapObject() throws Exception { + File serializeFile = new File(temporaryFolder, "testclass.bin"); + Map sourceObject = new HashMap<>(); + sourceObject.put(new TestClass1(), new TestClass2()); + + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(serializeFile)); + try { + outputStream.writeObject(sourceObject); + outputStream.flush(); + } finally { + outputStream.close(); + } + + String serialFilter; + Exception result; + + // default + assertNull(readSerializedObjectSF(null, serializeFile)); + + // now deny TestClass1 - key + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // now deny TestClass2 - value + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass2"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // now allow the key, should fail too because value is forbidden (and HashMap) + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // now allow the value, should fail too because the key is forbidden (and HashMap) + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass2;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // both key and value are in the allow list, it should fail because HashMap not permitted + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1;" + + "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass2;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // now add HashMap, test should pass. + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1;" + + "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass2;" + + "java.util.HashMap;java.util.Map$Entry;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + } + + // serialFilter version of testAllowDenyListAnonymousObject() + @Test + public void testSerialFilterAnonymousObject() throws Exception { + File serializeFile = new File(temporaryFolder, "testclass.bin"); + + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(serializeFile)); + try { + Serializable object = EnclosingClass.anonymousObject; + assertTrue(object.getClass().isAnonymousClass()); + outputStream.writeObject(object); + outputStream.flush(); + } finally { + outputStream.close(); + } + + String serialFilter; + Exception result; + + // default + assertNull(readSerializedObjectSF(null, serializeFile)); + + // forbidden by specifying the enclosing class + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.EnclosingClass*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // do it in allow List + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.EnclosingClass*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + } + + // serialFilter version of testAllowDenyListLocalObject() + @Test + public void testSerialFilterLocalObject() throws Exception { + File serializeFile = new File(temporaryFolder, "testclass.bin"); + + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(serializeFile)); + try { + Object object = EnclosingClass.getLocalObject(); + assertTrue(object.getClass().isLocalClass()); + outputStream.writeObject(object); + outputStream.flush(); + } finally { + outputStream.close(); + } + + String serialFilter; + Exception result; + + // default + assertNull(readSerializedObjectSF(null, serializeFile)); + + // forbidden by specifying the enclosing class + serialFilter = "!org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.EnclosingClass*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // do it in allow List + serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.EnclosingClass*;!*"; + result = readSerializedObjectSF(serialFilter, serializeFile); + assertNull(result); + } + + @Test + public void testSerialFilterSystemProperty() throws Exception { + File serializeFile = new File(temporaryFolder, "testclass.bin"); + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(serializeFile)); + try { + outputStream.writeObject(new TestClass1()); + outputStream.flush(); + } finally { + outputStream.close(); + } + + ConnectionFactoryOptions options = new ConnectionFactoryProperties(); + + Exception result; + + // default + assertNull(readSerializedObject(options, serializeFile)); + + // Set system property filter to block everything + System.setProperty(ObjectInputFilterFactory.SERIAL_FILTER_PROPERTY, "!*"); + try { + // Default filter should be read from system property + result = readSerializedObject(options, serializeFile); + assertTrue(result instanceof InvalidClassException); + + // Set options to allow all, this should override the default system property + options.setSerialFilter("*"); + assertNull(readSerializedObject(options, serializeFile)); + } finally { + System.clearProperty(ObjectInputFilterFactory.SERIAL_FILTER_PROPERTY); + } + } + + @Test + public void testObjectInputFilterFactoryPatternCache() { + String serialFilter = "org.apache.activemq.artemis.tests.unit.util.deserialization.pkg1.TestClass1;!*"; + + ObjectInputFilter oif1 = ObjectInputFilterFactory.getObjectInputFilterForPattern(serialFilter); + ObjectInputFilter oif2 = ObjectInputFilterFactory.getObjectInputFilterForPattern(serialFilter); + ObjectInputFilter oif3 = ObjectInputFilterFactory.getObjectInputFilterForPattern(new String(serialFilter)); + + assertSame(oif1, oif2); + assertSame(oif1, oif3); + } + + @Test + public void testObjectInputFilterFactoryClassNameCache() { + String className = AlwaysRejectObjectInputFilter.class.getName(); + + ObjectInputFilter oif1 = ObjectInputFilterFactory.getObjectInputFilterForClassName(className); + ObjectInputFilter oif2 = ObjectInputFilterFactory.getObjectInputFilterForClassName(className); + ObjectInputFilter oif3 = ObjectInputFilterFactory.getObjectInputFilterForClassName(new String(className)); + + assertSame(oif1, oif2); + assertSame(oif1, oif3); + } + + @Test + public void testObjectInputFilter() throws Exception { + File serializeFile = new File(temporaryFolder, "testclass.bin"); + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(serializeFile)); + try { + outputStream.writeObject(new TestClass1()); + outputStream.flush(); + } finally { + outputStream.close(); + } + + ConnectionFactoryOptions options = new ConnectionFactoryProperties(); + + Exception result; + + // default + assertNull(readSerializedObject(options, serializeFile)); + + // always accept filter + options.setSerialFilterClassName(AlwaysAcceptObjectInputFilter.class.getName()); + assertNull(readSerializedObject(options, serializeFile)); + + // always reject filter + options.setSerialFilterClassName(AlwaysRejectObjectInputFilter.class.getName()); + result = readSerializedObject(options, serializeFile); + assertTrue(result instanceof InvalidClassException); + } + private Exception readSerializedObject(String allowList, String denyList, File serailizeFile) { Exception result = null; @@ -499,8 +880,57 @@ private Exception readSerializedObject(String allowList, String denyList, File s return result; } + private Exception readSerializedObjectSF(String serialFilter, File serializeFile) { + Exception result = null; + + ObjectInputStreamWithClassLoader ois = null; + + try { + ois = new ObjectInputStreamWithClassLoader(new FileInputStream(serializeFile)); + + ObjectInputFilter oif = ObjectInputFilterFactory.getObjectInputFilterForPattern(serialFilter); + if (oif != null) { + ois.setObjectInputFilter(new DebugObjectInputFilter(oif)); + } + + ois.readObject(); + } catch (Exception e) { + result = e; + } finally { + try { + ois.close(); + } catch (IOException e) { + result = e; + } + } + return result; + } + + private Exception readSerializedObject(ConnectionFactoryOptions options, File serializeFile) { + Exception result = null; + + ObjectInputStreamWithClassLoader ois = null; + try { + ois = new ObjectInputStreamWithClassLoader(new FileInputStream(serializeFile)); + ObjectInputFilter oif = ObjectInputFilterFactory.getObjectInputFilter(options); + if (oif != null) { + ois.setObjectInputFilter(new DebugObjectInputFilter(oif)); + } + + ois.readObject(); + } catch (Exception e) { + result = e; + } finally { + try { + ois.close(); + } catch (IOException e) { + result = e; + } + } + return result; + } public static class ProxyReader implements Runnable { @@ -615,4 +1045,45 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } + + private static class DebugObjectInputFilter implements ObjectInputFilter { + private static final Logger logger = LoggerFactory.getLogger("objectfilter"); + + private ObjectInputFilter delegate; + + DebugObjectInputFilter(ObjectInputFilter delegate) { + this.delegate = delegate; + } + + @Override + public Status checkInput(FilterInfo filterInfo) { + Status status = delegate != null ? delegate.checkInput(filterInfo) : Status.UNDECIDED; + + if (logger.isDebugEnabled()) { + logger.debug("checkInput(): serialClass = {}, arrayLength = {}, depth = {}, references = {}, streamBytes = {}, STATUS = {}", + filterInfo.serialClass(), + filterInfo.arrayLength(), + filterInfo.depth(), + filterInfo.references(), + filterInfo.streamBytes(), + status); + } + + return status; + } + } + + public static class AlwaysRejectObjectInputFilter implements ObjectInputFilter { + @Override + public Status checkInput(FilterInfo filterInfo) { + return Status.REJECTED; + } + } + + public static class AlwaysAcceptObjectInputFilter implements ObjectInputFilter { + @Override + public Status checkInput(FilterInfo filterInfo) { + return Status.ALLOWED; + } + } }