//nefariousplan

CVE-2026-40858: Same Five Lines. Same Author. Eight Apache Camel Components.

patterns

cve

proof of concept

The Apache Camel advisory for CVE-2026-40858 names three prior CVEs of the same class. The git log of apache/camel names eight. Andrea Cosentino signed the commit that added ObjectInputFilter to camel-sql on January 9, 2024 and the commit that added it to camel-infinispan on April 14, 2026. Same author. Same monorepo. Same five lines. CVE-2026-40858 is the eighth time the same fix shipped to a different Camel component in twenty-seven months.

The Infinispan cache is the input to readObject()

Apache Camel's InfinispanRemoteAggregationRepository keeps in-flight aggregations in a remote Infinispan cluster. Camel routes that aggregate messages over time (a recovery store, a delayed completion, a correlated batch) call add(camelContext, key, exchange), which marshals the Exchange into a DefaultExchangeHolder and writes that holder to the cache under the correlation key:

// components/camel-infinispan/camel-infinispan-common/src/main/java/
//   org/apache/camel/component/infinispan/InfinispanAggregationRepository.java
@Override
public Exchange add(final CamelContext camelContext, final String key, final Exchange exchange) {
    DefaultExchangeHolder newHolder = DefaultExchangeHolder.marshal(exchange, true, allowSerializedHeaders);
    DefaultExchangeHolder oldHolder = getCache().put(key, newHolder);
    return unmarshallExchange(camelContext, oldHolder);
}

The cache value type is DefaultExchangeHolder. The on-the-wire representation is Infinispan's ProtoStream wire format wrapping exactly one field, and that field is the byte output of java.io.ObjectOutputStream.writeObject():

// components/camel-infinispan/camel-infinispan/src/main/java/
//   org/apache/camel/component/infinispan/remote/protostream/DefaultExchangeHolderProtoAdapter.java
@ProtoAdapter(DefaultExchangeHolder.class)
final class DefaultExchangeHolderProtoAdapter {
    @ProtoFactory
    public DefaultExchangeHolder create(byte[] serializedExchangeHolderBytes) {
        return DefaultExchangeHolderUtils.deserialize(serializedExchangeHolderBytes);
    }

    @ProtoField(number = 1)
    public byte[] serializedExchangeHolderBytes(DefaultExchangeHolder defaultExchangeHolder) {
        return DefaultExchangeHolderUtils.serialize(defaultExchangeHolder);
    }
}

ProtoStream presents itself as a typed schema. The schema has one field. The field is a byte blob. ProtoStream hands the blob to DefaultExchangeHolderUtils.deserialize, which calls ObjectInputStream.readObject() on it.

The aggregation repository's other public operations all funnel through the same path. get(key) returns unmarshallExchange(camelContext, getCache().get(key)). recover(exchangeId) returns unmarshallExchange(camelContext, getCache().get(exchangeId)). scan(camelContext) iterates keys whose values will be unmarshalled on the next get. Every public read on the repository ends in readObject.

The threat model in one sentence: anyone who can write a byte string into the Infinispan cache that a Camel route is reading from is the input to ObjectInputStream.readObject() running inside the Camel JVM. The CVSS vector reads AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H because "write access to the Infinispan cache" is the only precondition. Infinispan caches are shared between Camel and whatever else writes to them: other Camel routes, other applications backed by the same cluster, anyone with the cache's credentials, anyone with network reach to a Hot Rod endpoint whose authentication has been weakened. The cache is the wire.

The pre-patch deserialize() is six lines

The file was created by James Netherton on June 9, 2022 as part of CAMEL-18044, the switch from JBossUserMarshaller to plain Java serialization. The deserialize method has been the same six lines for forty-six months:

// pre-patch, June 9, 2022 through April 14, 2026
static DefaultExchangeHolder deserialize(byte[] bytes) {
    try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
         ObjectInputStream ois = new ClassLoadingAwareObjectInputStream(bais)) {
        return (DefaultExchangeHolder) ois.readObject();
    } catch (IOException | ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

ClassLoadingAwareObjectInputStream is a Camel utility that lets the Camel ClassLoader see classes from deployed bundles. It does not filter. It does not check. It hands bytes to readObject() and trusts whatever class the byte stream names. The cast on the return value runs after readObject returns, which is after every readObject and readResolve method in the type graph has already executed.

The April 14, 2026 patch (CAMEL-23322, commit ec297f8906, signed by Andrea Cosentino) adds five lines:

 static DefaultExchangeHolder deserialize(byte[] bytes) {
     try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
          ObjectInputStream ois = new ClassLoadingAwareObjectInputStream(bais)) {
+        ObjectInputFilter jvmFilter = ObjectInputFilter.Config.getSerialFilter();
+        if (jvmFilter != null) {
+            ois.setObjectInputFilter(jvmFilter);
+        } else {
+            LOG.debug("No JVM-wide deserialization filter set, applying default Camel filter: {}",
+                    DEFAULT_DESERIALIZATION_FILTER);
+            ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter(DEFAULT_DESERIALIZATION_FILTER));
+        }
         return (DefaultExchangeHolder) ois.readObject();
     } catch (IOException | ClassNotFoundException e) {
         throw new RuntimeException(e);
     }
 }

The same commit adds the constant DEFAULT_DESERIALIZATION_FILTER to the same file:

static final String DEFAULT_DESERIALIZATION_FILTER = "java.**;javax.**;org.apache.camel.**;!*";

That is the entire fix. Honor the JVM-wide filter set by jdk.serialFilter; otherwise apply an allowlist of java.**, javax.**, and org.apache.camel.**, and deny everything else. Five lines. The commit also adds a 63-line test (DefaultExchangeHolderUtilsTest) and a 37-line NotAllowedSerializable fixture under src/test/java/com/example/external/ to confirm the filter rejects unlisted types.

The fix already shipped, seven times, to the same monorepo

The Apache Camel source tree at commit ec297f8906 contains seven prior commits that wrote ObjectInputFilter into the deserialization path of seven other Camel components, signed by the same author:

$ git log --all --oneline --grep="ObjectInputFilter\|deserialization filter" -i --author=ancosen
1b60801edf CAMEL-23372: Tighten default ObjectInputFilter to deny java.net.** before java.**
34e59c6823 CAMEL-23321: JMS deserializationFilter should be global configurable ...
e735f56b4b CAMEL-23324: Add deserialization filtering for camel-vertx-http and camel-netty-http helper utilities
107e8c279c CAMEL-23321: Add deserialization filtering for JMS ObjectMessage hand...
ec297f8906 CAMEL-23322: Add deserialization filtering to camel-infinispan remote aggregation repository
c35b0a3720 CAMEL-23319: Add deserialization filtering to camel-mina converter
72201ec4d2 CAMEL-23297: Add deserialization filtering to camel-netty converters and codecs
55dd9f8ce5 CAMEL-23029 - Camel-Consul: Add ObjectInputFilter String pattern parameter in ConsulRegistry to be used in deserialize operations
deb971156b CAMEL-22966 - Camel-LevelDB: Add ObjectInputFilter String pattern parameter in LevelDBAggregationRepository to be used in unmarshall operations
c7aa49f8c8 CAMEL-20306 - Camel-CassandraQL: Add ObjectInputFilter String pattern parameter in CassandraAggregationRepository to be used in unmarshall operations
c23df082b0 CAMEL-20303 - Camel-Sql: Add ObjectInputFilter String pattern parameter in JdbcAggregationRepository to be used in unmarshall operations

Laid out chronologically:

Date JIRA Component CVE
2024-01-09 CAMEL-20303 camel-sql JdbcAggregationRepository CVE-2024-22369
2024-01-11 CAMEL-20306 camel-cassandraql CassandraAggregationRepository CVE-2024-23114
2026-02-06 CAMEL-22966 camel-leveldb LevelDBAggregationRepository CVE-2026-25747
2026-02-18 CAMEL-23029 camel-consul ConsulRegistry (no CVE)
2026-04-14 CAMEL-23297 camel-netty converters and codecs (no CVE)
2026-04-14 CAMEL-23319 camel-mina converter CVE-2026-40473
2026-04-14 CAMEL-23321 camel-jms ObjectMessage handler (no CVE)
2026-04-14 CAMEL-23322 camel-infinispan DefaultExchangeHolderUtils CVE-2026-40858
2026-04-14 CAMEL-23324 camel-vertx-http and camel-netty-http helpers (no CVE)

Twenty-seven months separate the camel-sql commit from the camel-infinispan commit. The five commits dated 2026-04-14 ship as one coordinated wave: the author was walking through the remaining components and adding ObjectInputFilter to every readObject() site that still lacked one. The first four had CVEs assigned because their attack paths cross obvious trust boundaries (a database, a Cassandra cluster, a LevelDB file, an Infinispan cache). Two of the April commits did not, because Cosentino fixed them in the same wave before anyone filed. The third, CAMEL-23319 (camel-mina), was assigned CVE-2026-40473 thirteen days later by Venkatraman Kumar at Securin, who read the same April 14 commit wave and filed against the row Apache's advisory had not named. The mina vector is the more network-direct of the two assigned numbers: where the infinispan path requires write access to a shared cache, MinaConverter.toObjectInput(IoBuffer) is reachable by any TCP or UDP packet to a Camel-MINA consumer port.

The Apache Camel CVE record for CVE-2026-40858 names three of those instances explicitly:

This issue follows the same class of vulnerability previously addressed in CVE-2024-22369, CVE-2024-23114 and CVE-2026-25747.

The CVE description names three. The git log names eight.

This is design-debt-driver. The substrate is DefaultExchangeHolder.

This is a design-debt-driver on Apache Camel, the same shape as CVE-2026-40453 on the same vendor one day earlier. There, the substrate was DefaultHeaderFilterStrategy.lowerCase = false, and the fix was one line copied into six subclasses across thirteen months. Here, the substrate is two earlier choices that produced the entire family: org.apache.camel.support.DefaultExchangeHolder implements Serializable, and the canonical pattern in the codebase for ferrying one over a network is new ObjectOutputStream(byteStream).writeObject(holder) on send, new ObjectInputStream(byteStream).readObject() on receive.

Both choices predate every CVE in the table above. They were the design at the moment camel-sql shipped JDBC aggregation. They were the design when camel-cassandraql cloned the JDBC aggregation pattern. They were the design when camel-leveldb did. They were the design when James Netherton wrote DefaultExchangeHolderUtils for camel-infinispan in June 2022, two and a half years before the same primitive produced a CVE in camel-sql. There is no shared DefaultExchangeHolderCodec module in apache/camel that the eight components import. There is no unmarshalWithFilter() helper in camel-support that the eight aggregation repositories call. Each component rolls its own ObjectInputStream. Each component's "fix" is its own copy of the same five lines.

That missing shared helper is the parallel-implementation-gap reading of the same evidence: eight Camel components implement the same wire protocol (Java serialization of DefaultExchangeHolder) against the same substrate, no module in camel-support owns the deserialization step, and so each component has to acquire the ObjectInputFilter independently. The design-debt-driver view says the substrate produced the family; the parallel-implementation-gap view says the missing shared codec is why hardening one instance never hardens the others. Both are true. Either one predicts a ninth component.

llama.cpp's RPC backend produces this shape in C: one deserializer reachable from three command handlers, three CVEs that each patched a handler, the deserializer unchanged. Camel produces it in Java with the boundaries inverted: one substrate (DefaultExchangeHolder plus the ObjectInputStream idiom) producing eight handlers, eight patches, each one patching its handler instead of the substrate. The shape is interchangeable. The next Camel component that adds remote persistence or a serialized-message transport is the ninth instance, and the only open question is which JIRA ticket assigns it.

CAMEL-23372 is the next admission

The CAMEL-23322 commit declares DEFAULT_DESERIALIZATION_FILTER = "java.**;javax.**;org.apache.camel.**;!*". Two weeks later, on April 28, 2026, the same author landed CAMEL-23372:

1b60801edf CAMEL-23372: Tighten default ObjectInputFilter to deny java.net.** before java.**

    Tightens the in-code default deserialization filter introduced by the
    CAMEL-23297 / CAMEL-23319 / CAMEL-23321 / CAMEL-23322 / CAMEL-23324
    series to explicitly deny classes under java.net.** before allowing the
    rest of java.**, javax.** and org.apache.camel.**. The recursive java.**
    allow in the previous default admitted classes whose hashCode / equals /
    readObject paths perform network I/O.

The patched constant now reads:

static final String DEFAULT_DESERIALIZATION_FILTER
        = "!java.net.**;java.**;javax.**;org.apache.camel.**;!*";

The filter Cosentino had been applying for twenty-seven months, the filter that closed CVE-2024-22369, CVE-2024-23114, CVE-2026-25747, and now CVE-2026-40858, allowed every class under java.net.**. java.net.URI's equals and hashCode are reachable through any number of gadget-chain shapes routing through java.util.HashMap.readObject. Cosentino needed a second patch to deny the java.net.** subtree he had been allowing. Eight components hardened with a filter that wasn't hard enough. CAMEL-23372 lists the five April JIRA tickets in its commit message because the same author updated all five files in the same diff.

What the advisory doesn't print

The Apache Camel advisory for CVE-2026-40858 marks the severity as "Important" and recommends upgrading to 4.14.7, 4.18.2, or 4.20.0. The CVE record adds CVSS 8.8 and the three prior CVEs. Neither artifact tells the reader that apache/camel has been doing this fix one component at a time since January 2024, with the same author, against the same idiom, with a filter that the author himself tightened two weeks after this commit landed. Neither artifact names CAMEL-23319 (camel-mina), the sibling commit that Securin filed as CVE-2026-40473 thirteen days later, against the more network-direct of the two attack paths the same wave produced. Apache's advisory pipeline assigned one number to the April wave. An outside reader of the same git log assigned a second. The commits Cosentino is signing are parseable by people other than Apache.

PoC: oss-security: CVE-2026-40858

DefaultExchangeHolder still implements Serializable. The substrate is the same in main as it was in camel-4.0.0-M3. The ninth component that adds remote transport will inherit the idiom, not the filter, and the only open question is which JIRA ticket assigns it.