YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Eclipse Deeplearning4j (.zip model) — unsafe Java deserialization (CWE-502) in ModelSerializer restore path: loading a malicious model executes attacker-controlled ObjectInputStream.readObject
Summary
Eclipse Deeplearning4j model files are .zip archives loaded with org.deeplearning4j.util.ModelSerializer.restoreMultiLayerNetwork(...) / restoreComputationGraph(...). On the standard, default load path, the restorer reads the archive entry preprocessor.bin and passes its bytes straight to new ObjectInputStream(...).readObject() with no ObjectInputFilter, no class allowlist. A malicious model .zip therefore drives Java native deserialization of fully attacker-controlled bytes the moment a victim loads it — a textbook CWE-502 untrusted-deserialization sink, executing attacker code inside readObject (demonstrated below). Neither picklescan nor modelscan inspects Java-serialized data, so the malicious model passes both scanners.
Affected / verified versions
- deeplearning4j-nn / deeplearning4j-core 1.0.0-M2.1 (latest published release, Jan 2023) on OpenJDK 21
- Sink confirmed identical and unpatched on
master(the restore helpers still callObjectInputStream.readObjectonpreprocessor.bin)
Root cause
ModelSerializer.class (bytecode, both restoreMultiLayerNetwork and restoreComputationGraph helpers):
319: ldc "preprocessor.bin"
347: new java/io/ObjectInputStream
353: invokespecial java/io/ObjectInputStream.<init>(InputStream)
360: invokevirtual java/io/ObjectInputStream.readObject() // <-- unfiltered deser of attacker bytes
The archive member preprocessor.bin is deserialized via a raw ObjectInputStream with no setObjectInputFilter, no whitelist, no validation. This runs on every standard restore call, independent of the loadUpdater flag (only updaterState.bin is flag-gated, and it goes through Nd4j.read, not readObject). Java deserialization runs the graph's readObject/readResolve/finalize chains during reconstruction, so attacker-reachable gadget classes execute code before any type check on the result.
Proof of concept
poc/ contains the malicious model, a benign control, and the Java harness.
poc/malicious_model.zip— a valid DL4J model (configuration.json+coefficients.bin) with one addedpreprocessor.binholding a serialized object whosereadObjectrunsRuntime.exec.poc/Evil.java— the demonstration gadget class (itsreadObjectexecutes a command and writes a marker).poc/Load.java—ModelSerializer.restoreMultiLayerNetwork(new File(argv[0]), true)(the standard API call).poc/benign_control.zip— same model withoutpreprocessor.bin.
Observed (reproduced)
$ java -cp out:libs/* Load malicious_model.zip
MARKER_DL4J_DESER_7Q3X note=served-as-preprocessor.bin whoami=uid=1002(hacnho) gid=1002(hacnho) groups=...,docker,kvm
Exception in thread "main" java.lang.ClassCastException: class Evil cannot be cast to ...DataSetPreProcessor
The command executed inside readObject (marker written with the running uid) before the cosmetic ClassCastException — i.e. attacker code runs to completion during deserialization; the cast failure afterward is irrelevant. A real gadget chain returns a value of the expected type (or the exception is simply swallowed by the caller) and leaves no trace.
Control: java -cp out:libs/* Load benign_control.zip → LOADED_OK params=23, exit 0, no marker — isolating the sink to the attacker-added preprocessor.bin.
Impact
Untrusted Java deserialization (CWE-502) on the default model-load path → remote code execution when a victim loads a malicious DL4J .zip model. This is the Java analog of the untrusted-pickle RCE class: the model file is attacker-controlled (the MFV threat model), and loading it deserializes attacker bytes through an unfiltered ObjectInputStream.
Honest scope on the gadget precondition: full host RCE requires a Serializable gadget chain reachable on the victim's classpath. The bare deeplearning4j-core dependency set ships commons-collections4-4.1, whose InvokerTransformer is not Serializable without -Dorg.apache.commons.collections4.enableUnsafeSerialization=true, and no other classic serializable-gadget library (commons-collections 3.x, commons-beanutils, c3p0, Spring, groovy, …) is in the default 172-jar set; JDK-21 serialization filters neutralize the JRE-internal chains. So on a bare library install the demonstrated impact is an arbitrary-deserialization primitive (CWE-502) that executes attacker code when a gadget is present (shown above with a class on the classpath). In practice DL4J is embedded in JVM applications/serving stacks that routinely pull gadget-bearing libraries (Spring, c3p0, commons-beanutils, etc.), making the precondition commonly satisfiable → RCE. The vulnerability is the unfiltered readObject sink itself; gadget availability is environmental and continually growing.
Remediation
- Wrap the
preprocessor.bin(and any otherreadObject) deserialization in a strictObjectInputFilterallowlisting only the expectedorg.nd4j…DataSetPreProcessortypes (and primitives), or replace native Java serialization with a safe format. - Reject/serialize preprocessors via a schema-validated, non-
ObjectInputStreammechanism.
Dedup / novelty
- No CVE / GHSA for
ModelSerializerJava deserialization at any version; no huntr per-repo report (huntr.com/repos/…/deeplearning4jshows only generic NPE/EOF bug reports). The sink is present and unpatched onmaster. - Distinct from the other model-deserialization CWEs (pickle, etc.) — this is Java native
ObjectInputStreamon a.zipDL4J model, an in-scope huntr MFV format (DL4J – Eclipse). - Scanner blind spot (bonus):
modelscan→ "No issues found";picklescan→ "Scanned files: 0" — neither understands JavaObjectOutputStreamdata.
Artifacts
poc/malicious_model.zip,poc/benign_control.zip,poc/Evil.java,poc/Load.java,poc/MakeEvil2.java.