
Reproducer for CVE-2026-40859 — Apache Camel camel-netty-http / camel-vertx-http producer-side unsafe deserialization of HTTP response bodies (RCE)
This project demonstrates a Java deserialization vulnerability in Apache Camel's camel-netty-http
and camel-vertx-http components, tracked as CVE-2026-40859. When a producer endpoint is configured
with transferException=true (or the component-level allowJavaSerializedObject=true), a backend HTTP
response with a failure status and Content-Type: application/x-java-serialized-object has its body
deserialized with a raw java.io.ObjectInputStream and no ObjectInputFilter. An attacker who
controls the backend the Camel producer talks to — a compromised service, or a man-in-the-middle on a
plain-HTTP connection — can return a crafted serialized object and, if a gadget chain is on the
classpath, achieve remote code execution on the Camel host.
Advisory: https://camel.apache.org/security/CVE-2026-40859.html
| Property | Value |
|---|---|
| Components | camel-netty-http, camel-vertx-http (producer side) |
| Affected Class | org.apache.camel.component.netty.http.NettyHttpHelper#deserializeJavaObjectFromStream (and VertxHttpHelper#deserializeJavaObjectFromStream) |
| CWE | CWE-502: Deserialization of Untrusted Data |
| Impact | Remote Code Execution (RCE) |
| Precondition | transferException=true (or allowJavaSerializedObject=true) + throwExceptionOnFailure=true (default) + attacker-controlled backend |
| Affected Versions | From 4.0.0 before 4.14.8, from 4.15.0 before 4.18.3, from 4.19.0 before 4.20.0 |
| Fixed Versions | 4.14.8, 4.18.3, 4.20.0 |
| JIRA | CAMEL-23324 |
| Reporter | Venkatraman Kumar (Securin) |
Not exploitable in the default configuration —
transferExceptiondefaults tofalse. The PoC enables it, as an application that wants remote-exception propagation would.
On a non-2xx response, the netty-http producer (with throwExceptionOnFailure=true, the default) builds
an exception from the response via populateNettyHttpOperationFailedException. If transferException is
on and the response carries the serialized-object content type, it deserializes the body:
// NettyHttpHelper.populateNettyHttpOperationFailedException(...) - affected version
if (transferException) {
String contentType = response.headers().get(NettyHttpConstants.CONTENT_TYPE);
if (NettyHttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT.equals(contentType)) { // application/x-java-serialized-object
InputStream is = exchange.getContext().getTypeConverter().convertTo(InputStream.class, response);
if (is != null) {
Object body = deserializeJavaObjectFromStream(is); // <-- sink
if (body instanceof Exception) {
return (Exception) body;
}
}
}
}
// NettyHttpHelper.deserializeJavaObjectFromStream(InputStream) - affected version
ObjectInputStream ois = new ObjectInputStream(is); // NO ObjectInputFilter
answer = ois.readObject(); // gadget fires here
The gadget executes inside readObject(), before the instanceof Exception check — so the payload
does not even have to be an exception. camel-vertx-http has the identical sink in
VertxHttpHelper.deserializeJavaObjectFromStream.
from("direct:call")
.to("netty-http://backend-host:PORT/path?transferException=true");
// throwExceptionOnFailure defaults to true
Any producer call whose backend responds 5xx + application/x-java-serialized-object triggers the sink.
The victim is the Camel producer (it performs the deserialization). The attacker controls the
backend it calls. In this self-contained PoC both roles run in the same JVM/container: an embedded
raw-socket HTTP server (MaliciousBackend) plays the attacker-controlled backend, and the Camel route
(VictimRoute) is the victim.
CVE-2026-40859/
├── pom.xml # camel-netty-http 4.18.2 + commons-collections 3.2.1 (gadget)
├── Dockerfile # runs the app (with --add-opens, needed only to build the gadget)
├── docker-compose.yml
├── README.md
└── src/main/
├── java/com/example/
│ ├── Application.java
│ ├── VictimRoute.java # victim: netty-http producer, transferException=true
│ ├── MaliciousBackend.java # attacker backend: 500 + serialized-object body on :9999
│ ├── Gadget.java # CommonsCollections6 gadget, fires during readObject()
│ └── ExploitController.java # /exploit/attack drives the producer call
└── 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 is started with
--add-opens java.base/java.util=ALL-UNNAMED— that flag is a gadget-construction detail, unrelated to the vulnerability.
mvn clean package -DskipTests
docker compose up -d --build
curl -s http://localhost:8080/exploit/attack
# -> producer threw ...NettyHttpOperationFailedException (expected)
#
# >>> RCE proof — /tmp/pwned exists: true
docker exec cve-2026-40859 ls -la /tmp/pwned
docker compose down
Any camel-netty-http / camel-vertx-http producer configured with transferException=true (or
allowJavaSerializedObject=true) that talks to a backend an attacker can control or intercept:
http://) producer connection substitutes the response.transferException=true (or component allowJavaSerializedObject=true).throwExceptionOnFailure=true (the default).5xx + application/x-java-serialized-object.commons-collections:3.2.1).Upgrade to 4.14.8 / 4.18.3 / 4.20.0. The fix constrains both helpers with a default
ObjectInputFilter allow-list (java.**;javax.**;org.apache.camel.**;!*), customisable via the new
deserializationFilter endpoint option or the JVM-wide -Djdk.serialFilter system property.
Until upgrading:
transferException=true / allowJavaSerializedObject=true on producers talking to
untrusted or network-reachable backends.https) for producer connections so responses cannot be substituted in transit.-Djdk.serialFilter=java.**;org.apache.camel.**;!*.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.