-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The Infinispan cache is the input to readObject()\n\nApache 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:\n\n```java\n// components/camel-infinispan/camel-infinispan-common/src/main/java/\n// org/apache/camel/component/infinispan/InfinispanAggregationRepository.java\n@Override\npublic Exchange add(final CamelContext camelContext, final String key, final Exchange exchange) {\n DefaultExchangeHolder newHolder = DefaultExchangeHolder.marshal(exchange, true, allowSerializedHeaders);\n DefaultExchangeHolder oldHolder = getCache().put(key, newHolder);\n return unmarshallExchange(camelContext, oldHolder);\n}\n```\n\nThe 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()`:\n\n```java\n// components/camel-infinispan/camel-infinispan/src/main/java/\n// org/apache/camel/component/infinispan/remote/protostream/DefaultExchangeHolderProtoAdapter.java\n@ProtoAdapter(DefaultExchangeHolder.class)\nfinal class DefaultExchangeHolderProtoAdapter {\n @ProtoFactory\n public DefaultExchangeHolder create(byte[] serializedExchangeHolderBytes) {\n return DefaultExchangeHolderUtils.deserialize(serializedExchangeHolderBytes);\n }\n\n @ProtoField(number = 1)\n public byte[] serializedExchangeHolderBytes(DefaultExchangeHolder defaultExchangeHolder) {\n return DefaultExchangeHolderUtils.serialize(defaultExchangeHolder);\n }\n}\n```\n\nProtoStream 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.\n\nThe 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`.\n\nThe 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.\n\n## The pre-patch deserialize() is six lines\n\nThe 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:\n\n```java\n// pre-patch, June 9, 2022 through April 14, 2026\nstatic DefaultExchangeHolder deserialize(byte[] bytes) {\n try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);\n ObjectInputStream ois = new ClassLoadingAwareObjectInputStream(bais)) {\n return (DefaultExchangeHolder) ois.readObject();\n } catch (IOException | ClassNotFoundException e) {\n throw new RuntimeException(e);\n }\n}\n```\n\n`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.\n\nThe April 14, 2026 patch (CAMEL-23322, commit `ec297f8906`, signed by Andrea Cosentino) adds five lines:\n\n```diff\n static DefaultExchangeHolder deserialize(byte[] bytes) {\n try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);\n ObjectInputStream ois = new ClassLoadingAwareObjectInputStream(bais)) {\n+ ObjectInputFilter jvmFilter = ObjectInputFilter.Config.getSerialFilter();\n+ if (jvmFilter != null) {\n+ ois.setObjectInputFilter(jvmFilter);\n+ } else {\n+ LOG.debug(\"No JVM-wide deserialization filter set, applying default Camel filter: {}\",\n+ DEFAULT_DESERIALIZATION_FILTER);\n+ ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter(DEFAULT_DESERIALIZATION_FILTER));\n+ }\n return (DefaultExchangeHolder) ois.readObject();\n } catch (IOException | ClassNotFoundException e) {\n throw new RuntimeException(e);\n }\n }\n```\n\nThe same commit adds the constant `DEFAULT_DESERIALIZATION_FILTER` to the same file:\n\n```java\nstatic final String DEFAULT_DESERIALIZATION_FILTER = \"java.**;javax.**;org.apache.camel.**;!*\";\n```\n\nThat 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.\n\n## The fix already shipped, seven times, to the same monorepo\n\nThe 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:\n\n```\n$ git log --all --oneline --grep=\"ObjectInputFilter\\|deserialization filter\" -i --author=ancosen\n1b60801edf CAMEL-23372: Tighten default ObjectInputFilter to deny java.net.** before java.**\n34e59c6823 CAMEL-23321: JMS deserializationFilter should be global configurable ...\ne735f56b4b CAMEL-23324: Add deserialization filtering for camel-vertx-http and camel-netty-http helper utilities\n107e8c279c CAMEL-23321: Add deserialization filtering for JMS ObjectMessage hand...\nec297f8906 CAMEL-23322: Add deserialization filtering to camel-infinispan remote aggregation repository\nc35b0a3720 CAMEL-23319: Add deserialization filtering to camel-mina converter\n72201ec4d2 CAMEL-23297: Add deserialization filtering to camel-netty converters and codecs\n55dd9f8ce5 CAMEL-23029 - Camel-Consul: Add ObjectInputFilter String pattern parameter in ConsulRegistry to be used in deserialize operations\ndeb971156b CAMEL-22966 - Camel-LevelDB: Add ObjectInputFilter String pattern parameter in LevelDBAggregationRepository to be used in unmarshall operations\nc7aa49f8c8 CAMEL-20306 - Camel-CassandraQL: Add ObjectInputFilter String pattern parameter in CassandraAggregationRepository to be used in unmarshall operations\nc23df082b0 CAMEL-20303 - Camel-Sql: Add ObjectInputFilter String pattern parameter in JdbcAggregationRepository to be used in unmarshall operations\n```\n\nLaid out chronologically:\n\n| Date | JIRA | Component | CVE |\n|---|---|---|---|\n| 2024-01-09 | CAMEL-20303 | camel-sql `JdbcAggregationRepository` | CVE-2024-22369 |\n| 2024-01-11 | CAMEL-20306 | camel-cassandraql `CassandraAggregationRepository` | CVE-2024-23114 |\n| 2026-02-06 | CAMEL-22966 | camel-leveldb `LevelDBAggregationRepository` | CVE-2026-25747 |\n| 2026-02-18 | CAMEL-23029 | camel-consul `ConsulRegistry` | (no CVE) |\n| 2026-04-14 | CAMEL-23297 | camel-netty converters and codecs | (no CVE) |\n| 2026-04-14 | CAMEL-23319 | camel-mina converter | CVE-2026-40473 |\n| 2026-04-14 | CAMEL-23321 | camel-jms `ObjectMessage` handler | (no CVE) |\n| **2026-04-14** | **CAMEL-23322** | **camel-infinispan `DefaultExchangeHolderUtils`** | **CVE-2026-40858** |\n| 2026-04-14 | CAMEL-23324 | camel-vertx-http and camel-netty-http helpers | (no CVE) |\n\nTwenty-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.\n\nThe Apache Camel CVE record for CVE-2026-40858 names three of those instances explicitly:\n\n> This issue follows the same class of vulnerability previously addressed in CVE-2024-22369, CVE-2024-23114 and CVE-2026-25747.\n\nThe CVE description names three. The git log names eight.\n\n## This is design-debt-driver. The substrate is DefaultExchangeHolder.\n\nThis is a [design-debt-driver](/patterns/design-debt-driver) on Apache Camel, the same shape as [CVE-2026-40453](/posts/camel-cve-2026-40453-five-subclasses-needed-the-same-line) 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.\n\nBoth 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.\n\nThat missing shared helper is the [parallel-implementation-gap](/patterns/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.\n\n[llama.cpp's RPC backend produces this shape in C](/posts/llama-cpp-cve-2026-34159-deserializer-three-cves-have-not-patched): 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.\n\n## CAMEL-23372 is the next admission\n\nThe 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:\n\n```\n1b60801edf CAMEL-23372: Tighten default ObjectInputFilter to deny java.net.** before java.**\n\n Tightens the in-code default deserialization filter introduced by the\n CAMEL-23297 / CAMEL-23319 / CAMEL-23321 / CAMEL-23322 / CAMEL-23324\n series to explicitly deny classes under java.net.** before allowing the\n rest of java.**, javax.** and org.apache.camel.**. The recursive java.**\n allow in the previous default admitted classes whose hashCode / equals /\n readObject paths perform network I/O.\n```\n\nThe patched constant now reads:\n\n```java\nstatic final String DEFAULT_DESERIALIZATION_FILTER\n = \"!java.net.**;java.**;javax.**;org.apache.camel.**;!*\";\n```\n\nThe 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.\n\n## What the advisory doesn't print\n\nThe 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.\n\nPoC: [oss-security: CVE-2026-40858](https://seclists.org/oss-sec/2026/q2/226)","closing_line":"`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.","hook_md":"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.","post_id":310,"slug":"camel-cve-2026-40858-same-five-lines-eight-components","title":"CVE-2026-40858: Same Five Lines. Same Author. Eight Apache Camel Components.","type":"initial","unreadable_sentence":"The CVE description names three. The git log names eight."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCahNGuQAKCRDeZjl4jgkQ Jm44AP9i4AozxnDs8GlmfZL2iSAHbnX5F8oSs2uUqm6VXhZqfgD9HcuHIbHJpWjs OUnQ0s9Pm1KYYUO1BBvCU5NjX9+/bAI= =5PKa -----END PGP SIGNATURE-----