
Reproducer for CVE-2026-40860 — Apache Camel camel-jms/sjms/amqp JMS ObjectMessage unsafe deserialization (RCE)
This project demonstrates a Java deserialization vulnerability in Apache Camel's camel-jms component
(and, transitively, camel-sjms, camel-sjms2, camel-amqp, camel-activemq, camel-activemq6),
tracked as CVE-2026-40860. JmsBinding.extractBodyFromJms() deserializes the payload of an incoming
JMS ObjectMessage via ObjectMessage.getObject() with , class allow-list
or deny-list. Because this runs whenever (the default) and Camel is a JMS ,
an attacker able to publish a crafted to a consumed queue/topic can achieve when a gadget chain is on the classpath.
ObjectInputFiltermapJmsMessage=trueObjectMessage| Property | Value |
|---|---|
| Components | camel-jms (+ camel-sjms, camel-sjms2, camel-amqp, camel-activemq, camel-activemq6) |
| Affected Class | org.apache.camel.component.jms.JmsBinding#extractBodyFromJms → jakarta.jms.ObjectMessage#getObject() |
| CWE | CWE-502: Deserialization of Untrusted Data |
| Impact | Remote Code Execution (RCE) |
| Trigger | Camel JMS consumer + mapJmsMessage=true (default) + an ObjectMessage the attacker can enqueue |
| Affected Versions | From 3.0.0 before 4.14.7, from 4.15.0 before 4.18.2, from 4.19.0 before 4.20.0 |
| Fixed Versions | 4.14.7, 4.18.2, 4.20.0 |
| JIRA | CAMEL-23321 |
| Reporter | Venkatraman Kumar (Securin) |
// JmsBinding.extractBodyFromJms(Exchange, Message) - affected version
if (message instanceof ObjectMessage objectMessage) {
Object payload = objectMessage.getObject(); // <-- deserializes with no ObjectInputFilter
if (payload instanceof DefaultExchangeHolder holder) {
...
}
return payload;
}
getObject() runs the JMS provider's ObjectInputStream.readObject() over the message body. Camel adds no
class filtering of its own, so a gadget chain on the classpath executes during deserialization.
The fix (4.14.7 / 4.18.2 / 4.20.0) adds a default ObjectInputFilter allow-list
(java.**;javax.**;org.apache.camel.**;!*), customisable via the new deserializationFilter endpoint
option or the JVM-wide -Djdk.serialFilter. Note Camel's own commit message for the fix:
this check runs after the JMS provider has already deserialized the payload. It prevents unexpected classes from being propagated to the route, but it cannot, on its own, stop gadget chains whose
readObject()fires inside the provider'sObjectInputStream. Complete protection requires configuring the JMS provider's own deserialization filter and/or the JVM-wide-Djdk.serialFilter.
So full protection = upgrade Camel + constrain the provider / JVM filter. This PoC uses an ActiveMQ
client with trustAllPackages=true (a common real-world setting) so the provider deserializes the payload;
on an affected Camel version nothing else stands in the way.
from("jms:queue:evil") // mapJmsMessage defaults to true
.log("Consumed: ${body.class.name}");
Merely receiving the ObjectMessage triggers the deserialization — the route body is irrelevant.
The victim is the Camel JMS consumer. The attacker is any producer that can publish to the queue. Both talk to a real Apache ActiveMQ Artemis broker running in Docker.
CVE-2026-40860/
├── pom.xml # camel-jms 4.18.1 + activemq-client 6.2.4 + commons-collections 3.2.1 (gadget)
├── Dockerfile # runs the app (--add-opens only to build the gadget)
├── docker-compose.yml # Artemis broker (quay.io) + the reproducer app
├── README.md
└── src/main/
├── java/com/example/
│ ├── Application.java
│ ├── JmsConfig.java # OpenWire ConnectionFactory (trustAllPackages=true) + jms component
│ ├── VictimRoute.java # victim: from("jms:queue:evil")
│ ├── Gadget.java # CommonsCollections6 gadget, fires during getObject()
│ └── ExploitController.java # attacker: publishes ObjectMessage(gadget) to the queue
└── resources/
└── application.properties
In a real attack the serialized bytes are produced offline by the attacker (e.g. with ysoserial); only the victim needs the gadget chain on its classpath. This PoC builds the gadget in-process for convenience, which is why the JVM runs with
--add-opens java.base/java.util=ALL-UNNAMED— a gadget-construction detail, unrelated to the vulnerability.
mvn clean package -DskipTests
docker compose up -d --build
This starts an Artemis broker (quay.io/artemiscloud/activemq-artemis-broker) and the reproducer app,
which connects to it over OpenWire.
curl -s http://localhost:8080/exploit/attack
# -> ObjectMessage published to queue 'evil'.
# camel-jms consumer called ObjectMessage.getObject() -> deserialization.
#
# >>> RCE proof — /tmp/pwned exists: true
docker exec cve-2026-40860 ls -la /tmp/pwned
docker compose down
Any Camel JMS consumer (camel-jms, camel-sjms, camel-sjms2, camel-amqp, camel-activemq,
camel-activemq6) reading from a destination an attacker can publish to — a shared broker, a topic with
open producers, a queue fed by an untrusted upstream — with mapJmsMessage=true (the default).
mapJmsMessage=true (default).ObjectMessage to the consumed destination.trustAllPackages=true, or a provider without
a restrictive filter).commons-collections:3.2.1).Upgrade to 4.14.7 / 4.18.2 / 4.20.0, and constrain deserialization end-to-end:
-Djdk.serialFilter=java.**;org.apache.camel.**;!* (or the endpoint's new
deserializationFilter option).trustedPackages, do not
use trustAllPackages=true).Until upgrading:
ObjectMessage payloads; set mapJmsMessage=false where the raw message is acceptable.trustAllPackages=true on untrusted destinations.-Djdk.serialFilter.This reproducer is provided for security research and authorized testing only, for a publicly disclosed and fixed vulnerability. Do not use it against systems without explicit permission.