From 6abb1771ecd3727051f045a34f7b1f12941719f5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:35:23 +0000 Subject: [PATCH 1/5] Fix insecure deserialization in ZookeeperDistributedQueue (CWE-502) Add allowlist-based class filtering to the deserialize() method to prevent Remote Code Execution via untrusted ObjectInputStream.readObject() calls. - Override resolveClass() with an anonymous ObjectInputStream that validates class names against a configurable set of allowed prefixes - Default allowlist includes org.broadleafcommerce.*, java.lang.*, java.util.*, java.math.*, java.time.*, and primitive array types - Extract isClassAllowed() as a protected method for subclass extensibility - Reject all classes not on the allowlist with InvalidClassException Co-Authored-By: Arjun Mishra --- .../util/queue/ZookeeperDistributedQueue.java | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java index 259089438f..d13b79e64f 100644 --- a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java +++ b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java @@ -38,18 +38,23 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -79,6 +84,27 @@ public class ZookeeperDistributedQueue implements Distri private static final Log LOG = LogFactory.getLog(ZookeeperDistributedQueue.class); private static final String QUEUE_ENTRY_NAME = "dz-queue-entry"; + /** + * Allowed package prefixes for deserialization. Only classes whose fully-qualified names start with one of these + * prefixes will be permitted during deserialization. This mitigates CWE-502 (Deserialization of Untrusted Data). + */ + private static final Set ALLOWED_DESERIALIZATION_PREFIXES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "org.broadleafcommerce.", + "java.lang.", + "java.util.", + "java.math.", + "java.time.", + "java.io.Serializable", + "[B", // byte arrays + "[C", // char arrays + "[I", // int arrays + "[J", // long arrays + "[D", // double arrays + "[F", // float arrays + "[S", // short arrays + "[Z" // boolean arrays + ))); + protected final Object QUEUE_MONITOR = new Object(); private final String queueFolderPath; private final ZooKeeper zk; @@ -822,7 +848,8 @@ public void process(WatchedEvent event) { } /** - * Mechanism to convert a byte array to an object. Default implementation uses {@link ObjectInputStream}. + * Mechanism to convert a byte array to an object. Default implementation uses a filtering {@link ObjectInputStream} + * that only permits classes from {@link #ALLOWED_DESERIALIZATION_PREFIXES} to be deserialized. * * @param bytes * @return @@ -831,7 +858,16 @@ protected Object deserialize(byte[] bytes) { ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = null; try { - ois = new ObjectInputStream(bais); + ois = new ObjectInputStream(bais) { + @Override + protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { + String className = desc.getName(); + if (!isClassAllowed(className)) { + throw new InvalidClassException(className, "Deserialization of class is not allowed: " + className); + } + return super.resolveClass(desc); + } + }; return ois.readObject(); } catch (IOException | ClassNotFoundException e) { throw new DistributedQueueException("Unable to deserialze an element from the Zookeeper queue.", e); @@ -856,6 +892,27 @@ protected Object deserialize(byte[] bytes) { } } + /** + * Determines whether a class is permitted for deserialization based on the configured allowlist. + * Array types of allowed classes are also permitted. + * + * @param className the fully-qualified class name to check + * @return true if the class is allowed, false otherwise + */ + protected boolean isClassAllowed(String className) { + for (String prefix : ALLOWED_DESERIALIZATION_PREFIXES) { + if (className.startsWith(prefix)) { + return true; + } + } + // Allow arrays of permitted types (e.g. "[Lorg.broadleafcommerce.SomeClass;") + if (className.startsWith("[L") && className.endsWith(";")) { + String elementClass = className.substring(2, className.length() - 1); + return isClassAllowed(elementClass); + } + return false; + } + /** * Mechanism to convert an object to a byte array. Default implementation uses {@link ObjectOutputStream}. * From 284f9f73b2ae5563184bdf5e63c3892ff379df78 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:40:01 +0000 Subject: [PATCH 2/5] Handle multi-dimensional arrays in deserialization allowlist Recursively strip leading '[' characters so types like byte[][] ("[[B") and String[][] ("[[Ljava.lang.String;") are correctly validated against the allowlist instead of being rejected. Co-Authored-By: Arjun Mishra --- .../core/util/queue/ZookeeperDistributedQueue.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java index d13b79e64f..3755909dae 100644 --- a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java +++ b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java @@ -910,6 +910,10 @@ protected boolean isClassAllowed(String className) { String elementClass = className.substring(2, className.length() - 1); return isClassAllowed(elementClass); } + // Handle multi-dimensional arrays (e.g. "[[B", "[[Ljava.lang.String;") + if (className.startsWith("[")) { + return isClassAllowed(className.substring(1)); + } return false; } From e21ff1afc28aba59cd0dd53a5eb54acc3ca887de Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:45:11 +0000 Subject: [PATCH 3/5] Remove dead java.io.Serializable entry from allowlist The entry was ineffective (resolveClass is never called for interfaces) and its prefix match inadvertently permitted java.io.SerializablePermission. Co-Authored-By: Arjun Mishra --- .../core/util/queue/ZookeeperDistributedQueue.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java index 3755909dae..497d04f679 100644 --- a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java +++ b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java @@ -94,7 +94,6 @@ public class ZookeeperDistributedQueue implements Distri "java.util.", "java.math.", "java.time.", - "java.io.Serializable", "[B", // byte arrays "[C", // char arrays "[I", // int arrays From e8045f97722fadfc380ed955273bed52155b2fc5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:50:26 +0000 Subject: [PATCH 4/5] Add org.apache.solr. to deserialization allowlist IncrementalUpdateCommand contains List fields from org.apache.solr.common, which must be permitted during deserialization. Co-Authored-By: Arjun Mishra --- .../core/util/queue/ZookeeperDistributedQueue.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java index 497d04f679..ec1d41c3e1 100644 --- a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java +++ b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java @@ -90,6 +90,7 @@ public class ZookeeperDistributedQueue implements Distri */ private static final Set ALLOWED_DESERIALIZATION_PREFIXES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "org.broadleafcommerce.", + "org.apache.solr.", "java.lang.", "java.util.", "java.math.", From d9cbf1a253d754f9aebcfb639a1786fde1afbe83 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:56:18 +0000 Subject: [PATCH 5/5] Override resolveProxyClass to block dynamic proxy deserialization bypass Without this, crafted streams using java.lang.reflect.Proxy with allowed interfaces could bypass the resolveClass filter entirely. Co-Authored-By: Arjun Mishra --- .../core/util/queue/ZookeeperDistributedQueue.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java index ec1d41c3e1..6a3daed4c0 100644 --- a/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java +++ b/core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java @@ -867,6 +867,16 @@ protected Class resolveClass(ObjectStreamClass desc) throws IOException, Clas } return super.resolveClass(desc); } + + @Override + protected Class resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { + for (String iface : interfaces) { + if (!isClassAllowed(iface)) { + throw new InvalidClassException(iface, "Deserialization of proxy interface is not allowed: " + iface); + } + } + return super.resolveProxyClass(interfaces); + } }; return ois.readObject(); } catch (IOException | ClassNotFoundException e) {