Conversation
Co-Authored-By: Arjun Mishra <arjunsaxmishra@gmail.com>
Co-Authored-By: Arjun Mishra <arjunsaxmishra@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| /** | ||
| * Allowlisted class patterns; the {@code ;!*} terminator is appended by {@link #buildDeserializationFilter(String)}. | ||
| */ | ||
| protected static final String DEFAULT_ALLOWED_CLASS_PATTERNS = "maxdepth=16;maxrefs=10000;maxbytes=1000000;maxarray=10000;java.lang.Boolean;java.lang.Byte;java.lang.Character;java.lang.Short;java.lang.Integer;java.lang.Long;java.lang.Float;java.lang.Double;java.lang.String;java.lang.Number;java.lang.Enum;java.math.BigDecimal;java.math.BigInteger;java.util.Date;java.util.UUID;java.util.ArrayList;java.util.LinkedList;java.util.HashMap;java.util.LinkedHashMap;java.util.TreeMap;java.util.HashSet;java.util.LinkedHashSet;java.util.TreeSet;java.util.Optional;java.time.*;org.broadleafcommerce.**;java.lang.Object;java.util.Map$Entry"; |
There was a problem hiding this comment.
🔴 Distributed incremental indexing rejects documents
In distributed Solr mode, DEFAULT_ALLOWED_CLASS_PATTERNS excludes SolrInputDocument, although every incremental command contains these documents. Polling fails before the index update executes.
Prompt for agents
The built-in distributed Solr queue stores IncrementalUpdateCommand instances containing org.apache.solr.common.SolrInputDocument objects. The default filter in core/broadleaf-framework/src/main/java/org/broadleafcommerce/core/util/queue/ZookeeperDistributedQueue.java permits Broadleaf command classes and standard collections but rejects the Solr document classes in their serialization graph. Audit a representative IncrementalUpdateCommand serialization graph and add the narrowly required Solr classes to the default allowlist. Add an integration-style filter test that round-trips an IncrementalUpdateCommand containing a populated SolrInputDocument, not only standard collection examples.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
| public void setAdditionalAllowedClassPatterns(String additionalAllowedClassPatterns) { | ||
| this.additionalAllowedClassPatterns = additionalAllowedClassPatterns; | ||
| this.deserializationFilter = null; |
There was a problem hiding this comment.
🟡 Concurrent allowlist updates stay stale
If setAdditionalAllowedClassPatterns overlaps filter construction, getDeserializationFilter can publish the old filter after invalidation. Later queue reads ignore the new patterns indefinitely.
Prompt for agents
The mutable additionalAllowedClassPatterns value and cached deserializationFilter are updated without one shared synchronization strategy. A thread in getDeserializationFilter can capture old patterns, race with setAdditionalAllowedClassPatterns invalidating the cache, and then repopulate that cache with the old filter. Make pattern updates and lazy filter publication atomic with respect to each other, using synchronization or an immutable holder containing both configuration and filter. Add a deterministic concurrency test or refactor so stale publication is structurally impossible.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
| @Test | ||
| public void shouldAllowAllowlistedClasses() throws Exception { | ||
| assertEquals(Integer.valueOf(42), deserialize(serialize(42), ZookeeperDistributedQueue.buildDeserializationFilter(null))); | ||
| assertEquals("value", deserialize(serialize("value"), ZookeeperDistributedQueue.buildDeserializationFilter(null))); | ||
|
|
||
| List<String> list = new ArrayList<>(); | ||
| list.add("value"); | ||
| assertEquals(list, deserialize(serialize(list), ZookeeperDistributedQueue.buildDeserializationFilter(null))); | ||
|
|
||
| HashMap<String, Integer> map = new HashMap<>(); | ||
| map.put("value", 42); | ||
| assertEquals(map, deserialize(serialize(map), ZookeeperDistributedQueue.buildDeserializationFilter(null))); | ||
| } | ||
|
|
||
| @Test(expected = InvalidClassException.class) | ||
| public void shouldRejectNonAllowlistedClasses() throws Exception { | ||
| deserialize(serialize(new File("value")), ZookeeperDistributedQueue.buildDeserializationFilter(null)); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldAllowAdditionalClassPatterns() throws Exception { | ||
| Object result = deserialize( | ||
| serialize(new File("value")), | ||
| ZookeeperDistributedQueue.buildDeserializationFilter("java.io.File") | ||
| ); | ||
|
|
||
| assertEquals(new File("value"), result); | ||
| } |
There was a problem hiding this comment.
| if (additionalPatterns != null && !additionalPatterns.trim().isEmpty()) { | ||
| effectivePatterns += ";" + additionalPatterns; | ||
| } | ||
| effectivePatterns += ";!*"; |
A Brief Overview
ZookeeperDistributedQueue.deserialize()calledObjectInputStream.readObject()on bytes read straight from Zookeeper znodes with no class filtering, so anyone able to write to the queue's znodes could drive gadget-chain deserialization and reach RCE (CWE-502).The build targets Java 17, so the fix uses the JDK's own
ObjectInputFilterrather than a customresolveClasssubclass:The filter is an allowlist terminated with
!*, so anything unlisted is rejected: JDK value types (boxed primitives,String,Number,Enum,BigDecimal/BigInteger,Date,UUID,java.time.*), the common collections,java.lang.Objectandjava.util.Map$Entry(needed for theObject[]/Map.Entry[]backing arrays JDK serialization emits for those collections), andorg.broadleafcommerce.**— queue elements areT extends Serializablesupplied by application code, and themaxCapacityconfig node is anInteger. Limitsmaxdepth=16;maxrefs=10000;maxbytes=1000000;maxarray=10000bound the resource cost of a hostile payload, since allowingObjectalso allowsObject[].Deployments that queue element types outside
org.broadleafcommerceextend the allowlist without subclassing:Extra patterns are inserted before the trailing
!*, and the assembled filter is cached in avolatilefield (invalidated by the setter) becausedeserializeruns on every poll. A rejected class surfaces as aDistributedQueueExceptionwhose message names the allowlist and the setter, instead of the generic "unable to deserialize" wrapper.Labels: Security, Severity: critical, Status: ready-for-code-review
Additional context
serialize()and all queue/locking behavior are unchanged. New testZookeeperDistributedQueueDeserializationFilterTestround-tripsInteger/String/ArrayList/HashMapthrough the filter, assertsjava.io.Fileis rejected withInvalidClassException, and assertsbuildDeserializationFilter("java.io.File")allows it — proving additional patterns land before the terminator. Verified withmvn -pl core/broadleaf-framework -am -DskipTests installandmvn -pl core/broadleaf-framework -Dtest=ZookeeperDistributedQueueDeserializationFilterTest test(3 tests, 0 failures).Devin-Org: engineering
Link to Devin session: https://app.devin.ai/sessions/b0d17dc9564143c09e3a9812d82abdf3
Open in Devin Desktop: https://app.devin.ai/desktop/session/b0d17dc9564143c09e3a9812d82abdf3?variant=devin
Requested by: @Colhodm
Devin Review
1c94d3f