
Reproducer for CVE-2026-43866 — Apache Camel camel-jms forged DefaultExchangeHolder bypass of the CVE-2026-40860 deserialization filter (Exchange-state injection)
This project demonstrates CVE-2026-43866, a bypass of the CVE-2026-40860 fix in Apache Camel's
camel-jms (and camel-sjms / JMS-family) components. CVE-2026-40860 added a post-deserialization class
allow-list (java.**;javax.**;org.apache.camel.**;!*) to incoming JMS ObjectMessage payloads. But
org.apache.camel.support.DefaultExchangeHolder lives in the allow-listed org.apache.camel.** namespace, so
an ObjectMessage whose top-level object is a DefaultExchangeHolder passes the check. The receiving side
then calls DefaultExchangeHolder.unmarshal() on it without requiring transferExchange — writing every
non-null field of the holder into the routed Exchange (body, IN/OUT headers, exchange properties, variables,
exchange id, exception). An attacker who can publish an ObjectMessage can thus inject arbitrary Exchange
state using only universally-trusted java.* types — no deserialization gadget chain required — to
manipulate routing, headers, properties and error handling.
Advisory: https://camel.apache.org/security/CVE-2026-43866.html
// JmsBinding.extractBodyFromJms(...) - affected 4.18.2
if (message instanceof ObjectMessage objectMessage) {
Object payload = objectMessage.getObject();
checkDeserializedClass(payload); // CVE-2026-40860 allow-list: java.**;javax.**;org.apache.camel.**;!*
if (payload instanceof DefaultExchangeHolder holder) { // <-- DefaultExchangeHolder is org.apache.camel.** -> passes
DefaultExchangeHolder.unmarshal(exchange, holder); // <-- writes forged state into the Exchange; NO transferExchange gate
Map<String, Object> jmsHeaders = extractHeadersFromJms(message, exchange);
exchange.getIn().getHeaders().putAll(jmsHeaders);
return exchange.getIn().getBody();
} else {
return payload;
}
}
The asymmetry: the sending side gates ObjectMessage/transferExchange creation, but the receiving
side unmarshals any DefaultExchangeHolder it deserializes. The fix (4.14.8 / 4.18.3 / 4.21.0) adds a new
objectMessageEnabled option (security = "insecure:serialization", default false) — an incoming
ObjectMessage is no longer deserialized at all unless explicitly enabled, so a forged holder can never reach
unmarshal(). (This is a breaking change for routes that rely on ObjectMessage / transferExchange.)
A JMS-provider allow-list does not help. This PoC configures the ActiveMQ client with a realistic, locked-down
trustedPackages = [java, javax, org.apache.camel]— theorg.apache.camelentry is exactly what a deployment using legitimatetransferExchangemust trust. The forged holder still passes, because it is itself aDefaultExchangeHolderwhose fields are all trustedjava.*types, indistinguishable from a legitimate one.
from("jms:queue:cve") // mapJmsMessage defaults to true; transferExchange NOT set
.process(exchange -> { /* observes the injected body / headers / properties */ });
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 in Docker.
CVE-2026-43866/
├── pom.xml # camel-jms 4.18.2 + activemq-client 6.2.4 (NO gadget library)
├── Dockerfile # runs the app (no --add-opens; no gadget)
├── docker-compose.yml # Artemis broker (quay.io) + the reproducer app
├── README.md
└── src/main/
├── java/com/example/
│ ├── Application.java
│ ├── JmsConfig.java # OpenWire ConnectionFactory (trustedPackages incl. org.apache.camel) + jms component
│ ├── VictimRoute.java # victim: from("jms:queue:cve"); records what the route observed
│ ├── CapturedState.java
│ ├── ForgedHolderFactory.java # builds a DefaultExchangeHolder via the public marshal() API
│ └── ExploitController.java # attacker: publishes the forged holder as an ObjectMessage
└── resources/
└── application.properties
mvn clean package -DskipTests
docker compose up -d --build
curl -s http://localhost:8080/exploit/attack
# -> Published a forged DefaultExchangeHolder as a JMS ObjectMessage to queue 'cve'.
# ...
# body = INJECTED-BODY-... (injected: true)
# header = pwned-header-... (injected: true)
# property = pwned-property-... (injected: true)
#
# >>> Exchange-state injection proof — attacker controlled body+header+property: true
The route ran with a body, header and property it never set — all supplied by the attacker's forged holder.
docker compose down
Any Camel JMS consumer (camel-jms, camel-sjms, camel-sjms2, camel-amqp, camel-activemq,
camel-activemq6) with mapJmsMessage=true (the default), reading from a destination an attacker can publish
to. transferExchange does not need to be enabled on the consumer.
mapJmsMessage=true (default), on an affected version.ObjectMessage whose payload is a DefaultExchangeHolder.transferExchange, means the
provider already trusts org.apache.camel. No gadget library is required.Upgrade to 4.14.8 / 4.18.3 / 4.21.0 (CAMEL-23373 / CAMEL-23409). JMS ObjectMessage handling is disabled by
default via the new objectMessageEnabled option; only enable it against destinations fed exclusively by
trusted producers.
Until upgrading:
ObjectMessage bodies to untrusted networks.DefaultExchangeHolder).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.
| Property | Value |
|---|
| Components | camel-jms (+ camel-sjms, camel-sjms2, camel-amqp, camel-activemq, camel-activemq6) |
| Affected Class | org.apache.camel.component.jms.JmsBinding#extractBodyFromJms → DefaultExchangeHolder.unmarshal |
| CWE | CWE-502 (Deserialization of Untrusted Data) + CWE-20 (Improper Input Validation) |
| Impact | Exchange-state injection: attacker-controlled body, headers, properties, variables, exception |
| Nature | Bypass of the CVE-2026-40860 class-filter fix (not a flaw in it) |
| Affected Versions | From 3.0.0 before 4.14.8, from 4.15.0 before 4.18.3, from 4.19.0 before 4.21.0 |
| Fixed Versions | 4.14.8, 4.18.3, 4.21.0 |
| JIRA | CAMEL-23373 (jms), CAMEL-23409 (sjms) |
| Reporter | gaorenyusi |