Skip to content

CVE-2026-43825: Verified Repro With Script Download

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.

REPRO-2026-00279 apache/opennlp · github Deserialization Variant found Jul 9, 2026 CVE entry ↗ .txt
Severity
HIGH
CVSS
7.3
Confidence
HIGH
Reproduced in
14m 59s
Tool calls
168
Spend
$2.27
01 · Overview

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).

02 · Severity & CVSS

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 threat level
7.3 / 10 CVSS base
Weakness CWE-502 Deserialization of Untrusted Data — Deserialization of Untrusted Data

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

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
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00279/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-43825

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Crafted Java serialized byte stream containing a MaliciousGadget object (Serializable class with readObject() side-effect) fed to SvmDoccatModel.deserialize(InputStream)

Attack chain
  1. SvmDoccatModel.deserialize()
  2. new ObjectInputStream(in)
  3. readObject()
  4. MaliciousGadget.readObject() [arbitrary code execution before cast to SvmDoccatModel]
Runnable proof: reproduction_steps.sh
Captured evidence: fixed oom smallfixed oom largefixed rce
Variants tested

Memory-exhaustion denial-of-service bypass of the 3.0.0-M4 fix for CVE-2026-43825, reproducing on the FIXED build (opennlp-3.0.0-M4, commit 3cf42d4a). The fix's ObjectInputFilter allow-lists java.util.HashMap and java.lang.String, allows any primitive array up to maxArrayLength (10,000,000), and bounds per-array lengt…

How the agent worked 366 events · 168 tool calls · 15 min
15 minDuration
168Tool calls
96Reasoning steps
366Events
1Dead-ends
Agent activity over 15 min
Support
18
Repro
131
Judge
45
Variant
168
0:0014:59

Root Cause and Exploit Chain for CVE-2026-43825

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)

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 — class opennlp.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 SvmDoccatModel instances from untrusted or semi-trusted sources. The method is public 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 MaliciousGadget class (implementing Serializable with a readObject() side-effect) was placed on the classpath and its readObject() method executed during SvmDoccatModel.deserialize(), writing a marker file as proof of arbitrary code execution. The cast to SvmDoccatModel threw ClassCastException only 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:

  1. An attacker crafts a serialized byte stream containing a gadget class (any Serializable class with a dangerous readObject() method).
  2. The stream is passed to SvmDoccatModel.deserialize().
  3. ObjectInputStream.readObject() encounters the gadget class, loads it from the classpath, and invokes its readObject() method — arbitrary code executes.
  4. Only then does the cast (SvmDoccatModel) fail with ClassCastException — 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 SvmDoccatModel serialization 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, causing readObject() to throw InvalidClassException BEFORE the foreign class is materialised.

Fix commit: 3cf42d4a0145cefd9dd06138873a91312052c767 Fix PR: https://github.com/apache/opennlp/pull/1029 (OPENNLP-1823)

Reproduction Steps

  1. Script: bundle/repro/reproduction_steps.sh
  2. 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 no ObjectInputFilter.
    • Compiles a MaliciousGadget class (a Serializable class whose readObject() writes a marker file) and a Poc harness that serializes the gadget and feeds the bytes to SvmDoccatModel.deserialize().
    • Runs the PoC against the vulnerable build: the gadget's readObject() executes, a marker file is created, and ClassCastException is thrown after execution.
    • Checks out and builds the fixed commit (3cf42d4a) which adds ObjectInputFilter.
    • Runs the same PoC against the fixed build: InvalidClassException is thrown by the filter, no code executes, no marker file is created.
    • Writes runtime_manifest.json with proof artifacts and exits 0 if the vulnerability is confirmed.
  3. Expected evidence:
    • bundle/logs/poc_vulnerable.log — shows [GADGET EXECUTED] and CODE EXECUTION CONFIRMED with exit code 10.
    • bundle/logs/gadget_executed_vulnerable.txt — the marker file written by the gadget's readObject().
    • bundle/logs/poc_fixed.log — shows ObjectInputFilter rejected foreign class with InvalidClassException and 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 to ObjectInputFilter (confirmed via grep -c).
  • Fixed SvmDoccatModel.java: 11 references to ObjectInputFilter (confirmed via grep -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 serialized SvmDoccatModel streams 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 to deserialize() 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 MaliciousGadget class 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-libsvm module only. Other OpenNLP modules use different serialization mechanisms (e.g., the main DoccatModel uses a custom binary format, not Java object serialization).
  • Limitation: The MaliciousGadget class 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.

Variant Analysis & Alternative Triggers for CVE-2026-43825

Versions: versions (as tested):Fixed: /vulnerable-to-this-variant: opennlp-3.0.0-M4 (commit

A denial-of-service bypass of the 3.0.0-M4 fix for CVE-2026-43825 was confirmed on the fixed code path. The fix (commit 3cf42d4a, opennlp-3.0.0-M4) hardens SvmDoccatModel.deserialize(InputStream) with an ObjectInputFilter whose class allow-list and numeric limits (graph depth, total references, per-array length) are intended to "bound pathological streams." However, the filter bounds per-array length and total reference count but not total allocated memory: it allow-lists java.util.HashMap and java.lang.String, allows any primitive array whose length does not exceed maxArrayLength (10,000,000), and counts each array as roughly one reference. A HashMap<String, double[8_000_000]> with a handful of entries is entirely allow-listed and within every numeric limit, yet deserialising it allocates n × 64 MiB. On a constrained heap this throws OutOfMemoryError inside ObjectInputStream.readObject()HashMap.readObject()readArray — i.e. during deserialisation on the fixed SvmDoccatModel.deserialize(), before the cast to SvmDoccatModel is ever attempted. The filter does not reject this stream (the large-heap run completes all allocations and then fails only at the cast with ClassCastException, never InvalidClassException).

This is a bypass of the fix's stated resource-limit protection, reaching the same sink (SvmDoccatModel.deserialize()ObjectInputStream.readObject()) and the same trust boundary (an attacker-controlled serialised stream supplied to deserialize()). The impact class is denial-of-service (memory exhaustion), not the original CVE's code-execution (RCE). An RCE bypass of the allow-list was not found and is explicitly ruled out below.

Fix Coverage / Assumptions

The fix (3cf42d4a, PR #1029, OPENNLP-1823) modifies exactly one production file — opennlp-core/opennlp-ml/opennlp-ml-libsvm/src/main/java/opennlp/tools/ml/libsvm/doccat/SvmDoccatModel.java — plus its test. It changes deserialize(InputStream) from:

public static SvmDoccatModel deserialize(InputStream in) throws IOException, ClassNotFoundException {
  try (ObjectInputStream ois = new ObjectInputStream(in)) {
    return (SvmDoccatModel) ois.readObject();
  }
}

to a version that installs an ObjectInputFilter before readObject(), adds a deserialize(InputStream, DeserializationLimits) overload, and adds a DeserializationLimits record. The filter (buildFilter):

  1. Rejects when depth > 64, references > 5,000,000, or arrayLength > 10,000,000.
  2. For each class descriptor: unwraps array component types; if the component type isPrimitive()ALLOWED (no class-name check, only the array-length bound above).
  3. Otherwise allows only the ~23 fully-qualified names in ALLOWED_CLASSES (OpenNLP doccat types, zlibsvm de.hhn.mi.* domain/config types, libsvm svm_model/svm_node/svm_parameter, and a minimal JDK set: String, Number, Integer, Double, Boolean, Enum, HashMap, Map$Entry); everything else → REJECTED.

Assumptions the fix relies on:

  • Foreign gadget classes (the original CVE's RCE mechanism) are not on the allow-list and are rejected before materialisation. (Verified: a MaliciousGadget stream is rejected with InvalidClassException: filter status: REJECTED on the fixed build — see RCE control below.)
  • The numeric limits "bound pathological streams" (per the javadoc: "defense in depth against pathological inputs").

What the fix does NOT cover:

  • Total allocated memory is unbounded. maxArrayLength bounds a single array; maxRefs bounds the count of references; neither bounds the sum of bytes allocated. n arrays of length up to 10,000,000 of an 8-byte primitive yield up to n × 80 MiB while staying within maxRefs (each array ≈ one reference) and maxArrayLength. With n in the hundreds, total allocation is tens of GiB — far beyond any consumer heap.
  • The class allow-list is type-blind for HashMap values: the filter checks class names per descriptor, not that a HashMap value conforms to the declared field type. A crafted SvmDoccatModel graph could carry HashMap<String,double[]> inside any HashMap field; the filter allows it and the OOM lands during readObject. (The PoC uses a top-level HashMap, which is sufficient because the OOM occurs during readObject, before the cast — the same "work happens before the cast" property as the original CVE.)
  • No other sink in the module is affectedSvmDoccatModel.deserialize() is the only ObjectInputStream.readObject() call in opennlp-ml-libsvm main source (confirmed by grep). Sibling modules use different mechanisms (BaseModel uses OpenNLP's custom binary/zip format; ObjectDataReader reads primitives only) and are out of the CVE's scope.

Variant / Alternate Trigger

Entry point (identical to the CVE): opennlp.tools.ml.libsvm.doccat.SvmDoccatModel.deserialize(InputStream) — the public static method in org.apache.opennlp:opennlp-ml-libsvm.

Alternate data path (the variant): instead of a foreign gadget class (rejected by the allow-list), the attacker supplies a stream built only from allow-listed classesHashMap<String, double[L]> with L ≤ 10,000,000 — that defeats the fix's resource-limit goal rather than its class-allow-list goal.

Code path: SvmDoccatModel.deserialize(InputStream) (line 236) → deserialize(InputStream, DeserializationLimits) (line 261) → ois.setObjectInputFilter(buildFilter(limits))ois.readObject() (line 271) → ObjectInputStream.readObject()HashMap.readObject()ObjectInputStream.readArray()java.lang.reflect.Array.newInstance()OutOfMemoryError (on a constrained heap), or full allocation + later ClassCastException (on a large heap).

Trigger path (machine-readable): attacker stream (HashMap<String,double[<=10M]>)SvmDoccatModel.deserializenew ObjectInputStreamsetObjectInputFilter(buildFilter) [filter ALLOWS HashMap/String/double[], arrayLength ≤ maxArrayLength, refs ≤ maxRefs] → readObject()HashMap.readObject()readArray allocates n×L×8 bytes → OutOfMemoryError (memory-exhaustion DoS) before cast.

Ruled-out candidates (bounded search):

  1. RCE bypass via an allow-listed gadget class — RULED OUT. Every allow-listed class was inspected (bytecode via javap / source):
    • de.hhn.mi.* (SvmConfigurationImpl, SvmModelImpl, SvmMetaInformationImpl, SvmFeatureImpl, SvmClassLabelImpl, NativeSvmModelWrapper) — records/data classes with only equals/hashCode, no readObject/readResolve/writeReplace.
    • libsvm.svm_model / svm_node / svm_parameter — plain Serializable data structs (public fields), no serialization hooks.
    • OpenNLP SvmDoccatConfiguration (plain Serializable), TermWeightingStrategy & FeatureSelectionStrategy (enums), SvmDoccatModel (no instance readObject/readResolve).
    • JDK allow-listed types: String, Number/Integer/Double/Boolean, Enum, HashMap, Map$Entry — none has a side-effecting readObject/readResolve that can invoke attacker-controlled code. No gadget chain can be built from allow-listed classes alone → no RCE bypass. The RCE control run confirms the foreign gadget is still rejected on the fixed build.
  2. Alternate entry point within the module — RULED OUT. SvmDoccatModel.deserialize() is the only ObjectInputStream.readObject() sink in opennlp-ml-libsvm main source.
  3. HashMap hash-collision CPU DoS — considered and deprioritised: HashMap + colliding String keys are allow-listed, but JDK 8+ treeifies buckets with >8 Comparable entries, reducing worst-case rebuild to ~O(N log N) (≈1e8 ops at the 5M ref limit, sub-second) — a weak DoS compared to the deterministic memory-exhaustion bypass above, which is not mitigated by any JDK mechanism.
  • Package/component affected: org.apache.opennlp:opennlp-ml-libsvmopennlp.tools.ml.libsvm.doccat.SvmDoccatModel.deserialize(InputStream).
  • Affected versions (as tested):
    • Fixed/vulnerable-to-this-variant: opennlp-3.0.0-M4 (commit 3cf42d4a0145cefd9dd06138873a91312052c767) — the bypass reproduces here.
    • Original CVE target (RCE): commit 3cb7232e (parent of fix, pre-M4) — RCE reproduces here; the DoS variant also reproduces here (trivially, no filter).
  • Risk level / consequences: Denial of service via Java heap exhaustion in any JVM process that calls SvmDoccatModel.deserialize() on an attacker- or semi-trusted-source stream. Because the filter does not bound total memory, a stream of a few hundred max-length primitive arrays (~tens of GiB of allocation) collapses the consumer's heap during readObject(). The fix's own javadoc concedes the filter is "defense in depth, not a license to deserialize from untrusted sources"; this variant shows that defense-in-depth is incomplete for memory-exhaustion.

Impact Parity

  • Disclosed/claimed maximum impact (parent CVE): code execution — arbitrary code execution via a gadget class on the consumer's classpath during deserialisation.
  • Reproduced impact from this variant run: denial-of-serviceOutOfMemoryError (Java heap space) during SvmDoccatModel.deserialize() on the fixed build, inside ObjectInputStream.readObject()HashMap.readObject()readArray.
  • Parity: partial — same sink, same trust boundary, same entry point, and a bypass of the fix's stated resource-limit protection; but a different (lesser) impact class (DoS, not RCE). The original CVE's RCE mechanism is not bypassed: the allow-list still rejects foreign gadget classes (verified by the RCE control run: InvalidClassException: filter status: REJECTED, no gadget execution on the fixed build).
  • Not demonstrated: code execution on the fixed build. No allow-listed class provides a gadget hook, so an RCE bypass was not achievable and is ruled out (see above).

Root Cause

The original root cause — ObjectInputStream.readObject() on an attacker-controlled stream — is only partially mitigated by the fix. The fix adds a class allow-list (which correctly closes the RCE vector) and numeric limits, but the numeric model is per-array and per-reference, not per-byte. ObjectInputFilter exposes depth(), references(), and arrayLength() but no cumulative allocated-byte metric. The fix's filter therefore cannot reject a stream whose class graph is benign and within limits yet whose total allocation is pathological. HashMap.readObject() faithfully allocates every double[] the stream requests; with each double[] up to 10,000,000 × 8 B and up to 5,000,000 references permitted, the upper bound on deserialisation memory is ~`5,000,000 × 80 MiB— astronomically beyond any real heap. The DoS is delivered duringreadObject()`, before the cast, exactly as the original CVE's gadget side-effects were.

Fix commit: 3cf42d4a0145cefd9dd06138873a91312052c767 ("OPENNLP-1823: Harden SvmDoccatModel.deserialize() with ObjectInputFilter and resource limits (#1029)", tag opennlp-3.0.0-M4).

Reproduction Steps

  1. Script: bundle/vuln_variant/reproduction_steps.sh
  2. What the script does:
    • Resolves the OpenNLP repo (prepared project cache or fresh clone) and creates two isolated git worktreeswt-vuln at 3cb7232e and wt-fixed at 3cf42d4a — without mutating the cache repo's HEAD.
    • Builds opennlp-ml-libsvm for each commit (reusing jars if present) and resolves the shared dependency classpath.
    • Compiles a Variant harness (build / oom / rce modes) and the MaliciousGadget class.
    • Builds the DoS payload: HashMap<String, double[8_000_000]> × 4 entries (256 MiB), serialised to /tmp/opennlp_variant_oom.bin.
    • Runs five scenarios against the appropriate build:
      1. fixed_oom_small — fixed build, -Xmx96mOutOfMemoryError during deserialize (bypass).
      2. fixed_oom_large — fixed build, -Xmx1gClassCastException after full allocation (filter allowed the payload, no rejection).
      3. vuln_oom_large — vulnerable build, -Xmx1gClassCastException (baseline; no filter).
      4. fixed_rce — fixed build + MaliciousGadgetInvalidClassException: filter status: REJECTED, no marker (fix blocks foreign-gadget RCE).
      5. vuln_rce — vulnerable build + MaliciousGadget → gadget executes, marker created (RCE parity baseline).
    • Classifies the logs and exits 0 iff the DoS bypass is confirmed on the fixed build and the fix still blocks foreign-gadget RCE; otherwise 1.
  3. Expected evidence:
    • bundle/logs/vuln_variant/fixed_oom_small.logOutOfMemoryError: Java heap space with stack through ObjectInputStream.readArrayHashMap.readObjectSvmDoccatModel.deserialize:271 (fixed).
    • bundle/logs/vuln_variant/fixed_oom_large.logClassCastException at SvmDoccatModel.deserialize:271 (filter allowed the payload).
    • bundle/logs/vuln_variant/fixed_rce.logInvalidClassException: filter status: REJECTED (RCE blocked).
    • bundle/logs/vuln_variant/vuln_rce.log + gadget_executed_vuln_variant.txt[GADGET EXECUTED] + marker (RCE on vulnerable).
    • bundle/logs/vuln_variant/fixed_version.txt, vulnerable_version.txt — exact tested commit identity.

Evidence

fixed_oom_small.log (the bypass — OOM during deserialize on the FIXED build):

VARIANT_OOM start in=/tmp/opennlp_variant_oom.bin file_bytes=256000155 calling SvmDoccatModel.deserialize() ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at java.base/java.lang.reflect.Array.newArray(Native Method)
        at java.base/java.lang.reflect.Array.newInstance(Array.java:78)
        at java.base/java.io.ObjectInputStream.readArray(ObjectInputStream.java:2150)
        at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1750)
        at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:540)
        ...
        at java.base/java.util.HashMap.readObject(HashMap.java:1560)
        ...
        at opennlp.tools.ml.libsvm.doccat.SvmDoccatModel.deserialize(SvmDoccatModel.java:271)
        at opennlp.tools.ml.libsvm.doccat.SvmDoccatModel.deserialize(SvmDoccatModel.java:236)
        at Variant.runOom(Variant.java:105)

fixed_oom_large.log (filter ALLOWS the payload — no rejection):

VARIANT_OOM start ... calling SvmDoccatModel.deserialize() ...
Exception in thread "main" java.lang.ClassCastException: class java.util.HashMap cannot be cast to class opennlp.tools.ml.libsvm.doccat.SvmDoccatModel ...
        at opennlp.tools.ml.libsvm.doccat.SvmDoccatModel.deserialize(SvmDoccatModel.java:271)

fixed_rce.log (fix's PRIMARY goal holds — foreign gadget rejected):

VARIANT_RCE stream_bytes=79 calling SvmDoccatModel.deserialize() ...
Exception in thread "main" java.io.InvalidClassException: filter status: REJECTED
        at opennlp.tools.ml.libsvm.doccat.SvmDoccatModel.deserialize(SvmDoccatModel.java:271)

Marker file: not created on the fixed build.

vuln_rce.log + gadget_executed_vuln_variant.txt (RCE parity baseline):

[GADGET EXECUTED] Arbitrary code ran during deserialization: id;whoami
[GADGET EXECUTED] Marker file written to: /tmp/opennlp_variant_marker.txt
Exception in thread "main" java.lang.ClassCastException: class MaliciousGadget ...
        at opennlp.tools.ml.libsvm.doccat.SvmDoccatModel.deserialize(SvmDoccatModel.java:196)

Marker content: DESERIALIZATION_GADGET_EXECUTED: id;whoami (Thread: main, Class: MaliciousGadget).

Environment:

  • JDK: OpenJDK 21.0.11; Maven 3.9.12; OS: Ubuntu 26.04 (Linux).
  • Variant target (bypass reproduces): 3cf42d4a0145cefd9dd06138873a91312052c767 (opennlp-3.0.0-M4).
  • Original CVE target: 3cb7232ecaccc788e32bf76b7c031fb166840da5 (parent of fix).
  • Cache repo HEAD left unchanged at 3cf42d4a (worktrees used for isolation).

Recommendations / Next Steps

To close the gap, the fix should bound total deserialisation cost, not only per-array length and reference count:

  1. Add a cumulative allocation budget. Track the sum of arrayLength × elementSize across all array allocations in the filter (or a wrapping InputStream/ObjectInputStream subclass) and reject when it exceeds a configurable byte budget (e.g. a few hundred MiB). ObjectInputFilter does not expose cumulative bytes, so this likely needs a custom ObjectInputStream that overrides readArray/allocation accounting, or pre-scanning the stream.
  2. Tighten primitive-array handling. Rather than allowing any primitive array up to maxArrayLength unconditionally, bound the aggregate primitive payload (e.g. reject when total primitive-array bytes exceed a limit), and/or lower maxArrayLength for the specific primitive types actually used by a legitimate SvmDoccatModel graph (double[], int[] for libsvm rho/label/nSV/sv_indices, svm_node[][]).
  3. Constrain HashMap value/key types. The filter is type-blind for map contents. Consider a stricter allow-list that rejects HashMap values that are arrays when the declared field type is not array-bearing, or cap the number of entries per HashMap.
  4. Keep defense-in-depth messaging. The javadoc already says callers must not deserialize from untrusted sources; retain and emphasise this — the filter is not a complete guarantee (this variant proves it for memory).

Additional Notes

  • Idempotency: reproduction_steps.sh was run three times consecutively; all runs exited 0 with identical verdicts (BYPASS_CONFIRMED=yes, FIX_RCE_BLOCKED=yes, VULN_RCE_WORKS=yes). It reuses existing worktrees, module jars, the dependency classpath, and the payload file when present.
  • Isolation / pipeline safety: Testing uses git worktree under bundle/vuln_variant/; the project-cache repo's HEAD is never changed (verified 3cf42d4a before and after). No checkout of the cache repo occurs.
  • Payload locality: The 256 MiB payload is kept in /tmp (not under bundle/) to avoid bloating the bundle; it is regenerated deterministically by the build mode if missing. The evidence is the log output, not the payload bytes.
  • Scope / trust boundary: The variant crosses the same trust boundary as the CVE — an attacker-controlled serialised stream supplied to SvmDoccatModel.deserialize() by a consumer that treats model bytes as semi-trusted. It is not a local-file-self-attack: the realistic threat is a downstream service that ingests model files from users/network and calls deserialize().
  • Limitation: The OOM is demonstrated with a purpose-built payload of allow-listed classes (simulating the memory-exhaustion mechanism). A real attacker would craft the same HashMap<String,double[]> graph; no exotic gadget library is required, which makes this bypass cheaper to mount than the original RCE (which needed a gadget-capable transitive dependency on the classpath).

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.

Event 1/40
0:000:44
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-43825 · REPRO-20
0:02
0:04
web search
0:06
web search
0:09
0:10
0:12
web search
0:15
0:16
web search
0:18
web search
0:26
0:26
extract_facts
no facts extracted
0:27
0:27
0:27
supportrepro
0:29
0:29
0:29
0:30
0:30
0:30
0:30
0:33
0:33
0:34
web search
0:36
0:37
0:38
web search
0:43
08 · How to Fix

How to Fix CVE-2026-43825

Upgrade apache/opennlp · github to 3.0.0-M4 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-43825 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-43825

How does the CVE-2026-43825 deserialization attack work?

An attacker crafts a serialized Java object stream containing a gadget chain and passes it to the public static SvmDoccatModel.deserialize() method. Because the method has no filtering, ObjectInputStream instantiates the gadget classes and triggers their side effects during deserialization, achieving code execution before the SvmDoccatModel cast even occurs. Apache OpenNLP itself does not ship a known gadget chain, so risk depends on gadget classes available via the consumer's transitive dependencies.

Which OpenNLP versions are affected by CVE-2026-43825, and where is it fixed?

Apache OpenNLP 3.0.0-M1 before 3.0.0-M4 is affected (only on the 3.x line, introduced with OPENNLP-1808). It is fixed in 3.0.0-M4, which adds an ObjectInputFilter and resource limits.

How severe is CVE-2026-43825?

It is rated high severity: remote code execution in any JVM process that loads SvmDoccatModel instances from untrusted or semi-trusted sources, since the deserialize method is public static and callable directly by any caller.

How can I reproduce CVE-2026-43825?

Download the verified script from this page and run it in an isolated environment against Apache OpenNLP 3.0.0-M1 through 3.0.0-M3 with the opennlp-ml-libsvm module. It feeds a crafted serialized object stream to SvmDoccatModel.deserialize() and shows the gadget chain executing on the vulnerable build, then confirms 3.0.0-M4 rejects it via ObjectInputFilter.
11 · References

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.