CVE-2026-43825: Verified Reproduction
CVE-2026-43825: Apache OpenNLP SvmDoccatModel unsafe deserialization
CVE-2026-43825 is verified against apache/opennlp · github. Affected versions: 3.0.0-M1 before 3.0.0-M4 (only on 3.x line; introduced in OPENNLP-1808). Fixed in 3.0.0-M4. Vulnerability class: Deserialization. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00279.
What Is CVE-2026-43825?
CVE-2026-43825 is a high-severity unsafe deserialization vulnerability (CWE-502) in Apache OpenNLP's SvmDoccatModel.deserialize(InputStream), part of the opennlp-ml-libsvm module. Pruva reproduced it (reproduction REPRO-2026-00279).
CVE-2026-43825 Severity & CVSS Score
CVE-2026-43825 is rated high severity, with a CVSS base score of 7.3 out of 10.
High — serious impact or readily exploitable. Prioritize remediation.
Affected apache/opennlp Versions
apache/opennlp · github versions 3.0.0-M1 before 3.0.0-M4 (only on 3.x line; introduced in OPENNLP-1808) are affected.
How to Reproduce CVE-2026-43825
pruva-verify REPRO-2026-00279 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00279/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-43825
- reached the target end-to-end
- full exploit chain demonstrated
- high confidence
- the upstream fix blocks the same trigger
Crafted Java serialized byte stream containing a MaliciousGadget object (Serializable class with readObject() side-effect) fed to SvmDoccatModel.deserialize(InputStream)
- SvmDoccatModel.deserialize()
- new ObjectInputStream(in)
- readObject()
- MaliciousGadget.readObject() [arbitrary code execution before cast to SvmDoccatModel]
How the agent worked
Root Cause and Exploit Chain for CVE-2026-43825
Apache OpenNLP 3.x before 3.0.0-M4 contains an unsafe Java deserialization vulnerability in SvmDoccatModel.deserialize(InputStream) within the opennlp-ml-libsvm module. The method creates a java.io.ObjectInputStream and calls readObject() on an attacker-supplied byte stream without installing an ObjectInputFilter. Because ObjectInputStream.readObject() materialises every class referenced in the stream before the result is cast to SvmDoccatModel, any gadget class available on the consumer's classpath can execute arbitrary code during deserialization — before the caller ever inspects the returned object.
- Package/component affected:
org.apache.opennlp:opennlp-ml-libsvm— classopennlp.tools.ml.libsvm.doccat.SvmDoccatModel - Affected versions: Apache OpenNLP 3.x before 3.0.0-M4 (the libsvm document categorization module was introduced via OPENNLP-1808 and exists only on the 3.x line)
- Fixed in: 3.0.0-M4 (commit
3cf42d4a0145cefd9dd06138873a91312052c767, PR #1029, OPENNLP-1823) - Risk level: High (CVSSv3 7.3 per Red Hat advisory)
- Consequences: Remote code execution in any JVM process that loads
SvmDoccatModelinstances from untrusted or semi-trusted sources. The method ispublic static, so any caller can pass an untrusted stream directly. Apache OpenNLP itself does not ship a known gadget chain, so the realistic risk is to downstream applications that embed the libsvm module alongside vulnerable transitive dependencies (e.g., CommonsCollections, Groovy, Spring).
Impact Parity
- Disclosed/claimed maximum impact: Code execution — arbitrary code execution via a crafted serialized stream when a gadget chain is present on the classpath.
- Reproduced impact from this run: Code execution — a custom
MaliciousGadgetclass (implementingSerializablewith areadObject()side-effect) was placed on the classpath and itsreadObject()method executed duringSvmDoccatModel.deserialize(), writing a marker file as proof of arbitrary code execution. The cast toSvmDoccatModelthrewClassCastExceptiononly AFTER the gadget code had already run. - Parity:
full— the proof demonstrates the exact mechanism described in the advisory:ObjectInputStream.readObject()materialises foreign objects before the cast, allowing arbitrary code execution during deserialization. - Not demonstrated: A real-world gadget chain (e.g., CommonsCollections) was not used; instead, a purpose-built gadget class simulated the same mechanism. This is appropriate because the CVE explicitly states "Apache OpenNLP itself does not ship a known gadget chain."
Root Cause
The vulnerable deserialize method in SvmDoccatModel.java (before 3.0.0-M4) is:
public static SvmDoccatModel deserialize(InputStream in) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(in)) {
return (SvmDoccatModel) ois.readObject();
}
}
No ObjectInputFilter is installed. Java's ObjectInputStream.readObject() resolves and instantiates every class descriptor in the serialized stream, invoking each class's readObject(), readResolve(), or other deserialization hooks as part of the process. The cast to SvmDoccatModel occurs only AFTER the entire object graph has been deserialised. This means:
- An attacker crafts a serialized byte stream containing a gadget class (any
Serializableclass with a dangerousreadObject()method). - The stream is passed to
SvmDoccatModel.deserialize(). ObjectInputStream.readObject()encounters the gadget class, loads it from the classpath, and invokes itsreadObject()method — arbitrary code executes.- Only then does the cast
(SvmDoccatModel)fail withClassCastException— but the damage is already done.
Fix (commit 3cf42d4a): The fix adds an ObjectInputFilter via ois.setObjectInputFilter(buildFilter(limits)) before readObject(). The filter:
- Allow-lists only the specific classes that can legitimately appear in a
SvmDoccatModelserialization graph (OpenNLP types, zlibsvm domain types, libsvm structures, and a minimal set of JDK types). - Bounds graph depth (64), references (5,000,000), and array length (10,000,000).
- Rejects any class not on the allow-list with
ObjectInputFilter.Status.REJECTED, causingreadObject()to throwInvalidClassExceptionBEFORE the foreign class is materialised.
Fix commit: 3cf42d4a0145cefd9dd06138873a91312052c767
Fix PR: https://github.com/apache/opennlp/pull/1029 (OPENNLP-1823)
Reproduction Steps
- Script:
bundle/repro/reproduction_steps.sh - What the script does:
- Installs JDK 21 and Maven (required by OpenNLP 3.x).
- Clones the Apache OpenNLP repository (or reuses the project cache).
- Checks out and builds the vulnerable commit (
3cb7232e) — the parent of the fix — which has noObjectInputFilter. - Compiles a
MaliciousGadgetclass (aSerializableclass whosereadObject()writes a marker file) and aPocharness that serializes the gadget and feeds the bytes toSvmDoccatModel.deserialize(). - Runs the PoC against the vulnerable build: the gadget's
readObject()executes, a marker file is created, andClassCastExceptionis thrown after execution. - Checks out and builds the fixed commit (
3cf42d4a) which addsObjectInputFilter. - Runs the same PoC against the fixed build:
InvalidClassExceptionis thrown by the filter, no code executes, no marker file is created. - Writes
runtime_manifest.jsonwith proof artifacts and exits 0 if the vulnerability is confirmed.
- Expected evidence:
bundle/logs/poc_vulnerable.log— shows[GADGET EXECUTED]andCODE EXECUTION CONFIRMEDwith exit code 10.bundle/logs/gadget_executed_vulnerable.txt— the marker file written by the gadget'sreadObject().bundle/logs/poc_fixed.log— showsObjectInputFilter rejected foreign classwithInvalidClassExceptionand exit code 20.
Evidence
Vulnerable version PoC output (bundle/logs/poc_vulnerable.log):
[*] Calling SvmDoccatModel.deserialize() with malicious stream...
[GADGET EXECUTED] Arbitrary code ran during deserialization: id;whoami;uname -a
[GADGET EXECUTED] Marker file written to: /tmp/opennlp_poc_marker_vuln.txt
[!] VULNERABLE version: readObject() completed, cast failed AFTER
[!] Exception: java.lang.ClassCastException: class MaliciousGadget cannot be cast to class opennlp.tools.ml.libsvm.doccat.SvmDoccatModel
[!!!] CODE EXECUTION CONFIRMED: gadget readObject() ran!
Gadget marker file (bundle/logs/gadget_executed_vulnerable.txt):
DESERIALIZATION_GADGET_EXECUTED: id;whoami;uname -a
Thread: main
Time: 2026-07-09T18:16:03.331869217Z
Class: MaliciousGadget
Fixed version PoC output (bundle/logs/poc_fixed.log):
[*] Calling SvmDoccatModel.deserialize() with malicious stream...
[+] FIXED version: ObjectInputFilter rejected foreign class
[+] Exception: java.io.InvalidClassException: filter status: REJECTED
[*] No marker file found — gadget code did NOT execute
VERDICT: FIXED — ObjectInputFilter blocked the foreign class
Source verification:
- Vulnerable
SvmDoccatModel.java: 0 references toObjectInputFilter(confirmed viagrep -c). - Fixed
SvmDoccatModel.java: 11 references toObjectInputFilter(confirmed viagrep -c).
Environment:
- JDK: OpenJDK 21.0.11
- Maven: 3.9.12
- OS: Ubuntu 26.04 (Linux)
- Vulnerable commit:
3cb7232ecaccc788e32bf76b7c031fb166840da5 - Fixed commit:
3cf42d4a0145cefd9dd06138873a91312052c767
Recommendations / Next Steps
- Upgrade: Users on OpenNLP 3.x before 3.0.0-M4 should upgrade to 3.0.0-M4 immediately.
- Defense in depth: Even with the
ObjectInputFilter, callers should treat serializedSvmDoccatModelstreams as untrusted input and verify their provenance before deserialization. The filter is defense-in-depth, not a license to deserialize from untrusted sources. - Input validation: Applications that accept model files from end users or third-party sources should add integrity checks (e.g., signatures, checksums) before calling
deserialize(). - Classpath hygiene: Remove unnecessary gadget-chain-capable libraries from the classpath of any process that deserializes OpenNLP models.
- Testing: The fix includes test cases (
SvmDoccatModelTest.java) that verify foreign classes are rejected. Downstream applications should add integration tests that feed crafted streams todeserialize()and verify rejection.
Additional Notes
- Idempotency: The script was run twice consecutively; both runs produced identical results (exit code 0, vulnerable exit 10, fixed exit 20).
- Gadget class: The
MaliciousGadgetclass is a purpose-built simulation of a real deserialization gadget. In a real attack, a class from a vulnerable transitive dependency (e.g.,org.apache.commons.collections.functors.InvokerTransformer) would serve the same role. The proof demonstrates the deserialization mechanism, not a specific real-world gadget chain. - Scope: The vulnerability is in the
opennlp-ml-libsvmmodule only. Other OpenNLP modules use different serialization mechanisms (e.g., the mainDoccatModeluses a custom binary format, not Java object serialization). - Limitation: The
MaliciousGadgetclass must be on the classpath for the exploit to work, which mirrors the real-world constraint that a gadget-chain-capable library must be present. The CVE description explicitly acknowledges this.
CVE-2026-43825 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-43825
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-43825
Upgrade apache/opennlp · github to 3.0.0-M4 or later.
FAQ: CVE-2026-43825
How does the CVE-2026-43825 deserialization attack work?
Which OpenNLP versions are affected by CVE-2026-43825, and where is it fixed?
How severe is CVE-2026-43825?
How can I reproduce CVE-2026-43825?
References for CVE-2026-43825
Authoritative sources for CVE-2026-43825 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.