You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

DL4J/ND4J SameDiffSerializer.loadInternal() unsafe Java deserialization (CWE-502) β€” RCE via .sdnb model manifest

Gated PoC β€” access granted to maintainers/triagers on request.

Target: github.com/eclipse/deeplearning4j, module nd4j-api, package org.nd4j.autodiff.samediff.serde, file SameDiffSerializer.java, private method loadInternal(File, boolean, SameDiff), called from the public API SameDiffSerializer.load(File, boolean).

Summary

SameDiffSerializer.loadInternal() parses a .sdnb ("SameDiff Native Blob") model file's header (magic bytes, version, and three offset/length fields), performs real bounds/consistency validation on those offsets, then reads the byte range it computed as the "manifest" and feeds those bytes directly into ObjectInputStream.readObject() with no type filter, no allow-list, and no ObjectInputFilter:

try (ByteArrayInputStream bais = new ByteArrayInputStream(
        manifestNio.array(), manifestNio.position(), manifestNio.remaining());
     ObjectInputStream ois = new ObjectInputStream(bais)) {
    manifest = (Map<String, Pair<Long, Long>>) ois.readObject();   // <-- CWE-502
} catch (Exception e) {
    throw new IOException("Failed to deserialize manifest from file: " + file.getAbsolutePath(), e);
}

readObject() executes before the checked cast to Map<String, Pair<Long, Long>> is evaluated. Standard Java deserialization semantics mean any class present on the victim application's classpath can run arbitrary code during readObject()/readResolve()/finalize() etc. (the classic "gadget chain" primitive, e.g. via ysoserial-style chains using commons-collections, already a declared DL4J/nd4j transitive dependency) β€” the eventual ClassCastException (because the attacker's class isn't really a Map) happens after the malicious side effect has already fired, so it does not prevent exploitation.

This is public API: any application that calls the documented SameDiffSerializer.load(File, boolean) (or SameDiff.load(...), which delegates to it) to load a .sdnb SameDiff model is directly exposed.

Not a duplicate

This is a separate, independent vulnerability from the two other already-known/already-filed DL4J deserialization issues:

  • Not CVE-2025-53001 / GHSA-wfhj-v5g7-vr7g β€” that advisory covers ModelSerializer.loadZipData() deserializing the preprocessor.bin ZIP entry (package org.deeplearning4j.util, module deeplearning4j-nn). Different file, different class, different module, different model format (MultiLayerNetwork/ComputationGraph .zip files vs. SameDiff .sdnb files).
  • Not the WordVectorSerializer finding β€” that one is WordVectorSerializer.readWordVectors(File) in org.deeplearning4j.models.embeddings.loader (module deeplearning4j-nlp), which deserializes the entire file with no header/format wrapper at all. Different file, different class, different module, different model type (word-vector models vs. SameDiff models).

SameDiffSerializer lives in nd4j-api (the ND4J math/autodiff library), not deeplearning4j-nn or deeplearning4j-nlp β€” a third, independent module with its own independent unsafe-deserialization bug.

Important scope note: SameDiffSerializer.java does not exist in the latest released Maven Central artifact (org.nd4j:nd4j-api:1.0.0-M2.1, 2022-08-10) β€” it is present on the current master branch only (added after that release, no newer release has been published since). This PoC compiles and runs the real, unmodified master-branch source of the vulnerable method directly (see "Methodology" below) since there is no released jar to depend on. The bug is real and live in the current upstream source that anyone building from master (or a future release once cut) would ship.

Attacker input β†’ sink

  1. Attacker crafts a .sdnb file with a valid 32-byte header (magic "SDNB", version, manifestOffset, manifestLength, metadataOffset) that passes loadInternal()'s own bounds checks (metadataOffset == HEADER_SIZE, manifestOffset >= metadataOffset, manifestOffset + manifestLength <= fileSize), followed by a Java serialization stream containing an attacker-controlled class instead of the expected Map<String, Pair<Long, Long>>.
  2. Victim application calls the public, documented SameDiffSerializer.load(someUntrustedFile, false) (or SameDiff.load(...)) to load the "model".
  3. loadInternal() validates the header (all real checks pass β€” this is not a parsing bug, the format is well-formed), reads the manifest byte range, and calls ObjectInputStream.readObject() on it directly.
  4. The attacker's class is instantiated and its readObject() method runs β€” arbitrary code execution, before any type check on the result.

Real, reproducible evidence (not just static reasoning)

Methodology

SameDiffSerializer.class is not present in any published nd4j-api jar on Maven Central (confirmed: jar tf nd4j-api-1.0.0-M2.1.jar | grep -i samediffserializer finds nothing). To test the real, current, unmodified source rather than a hand-written reproduction of the pattern, this PoC:

  1. Fetches SameDiffSerializer.java verbatim from https://raw.githubusercontent.com/eclipse/deeplearning4j/master/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/autodiff/samediff/serde/SameDiffSerializer.java (fetched 2026-07-09; included here as SameDiffSerializer_real_master_stubbed_for_standalone_build.java).
  2. Compiles it against the real, unmodified, released org.nd4j:nd4j-api:1.0.0-M2.1 jar from Maven Central (which supplies every other class this file needs β€” SameDiff, Pair, FlatGraph, etc. β€” since nd4j-api's own package evolved additively around this file).
  3. Seven unrelated methods (loadSharded, deserializeFromFlatBuffers, createSubInstancesVector, serializeMetadataFlatBuffer, serializeSmallNdArrayToFlatBuffer, deserializeSmallNdArrayFromInlineBuffer, createGraphShard) fail to compile against the older 1.0.0-M2.1 jar because they call newer FlatBuffers-schema/SameDiff API methods added to other files after that release (e.g. SameDiff.variableNames(), FlatGraph.metadataKeysLength()). None of these methods are on the vulnerable code path β€” loadInternal()'s header-parsing and ObjectInputStream.readObject() call (the actual vulnerability) is reached and fully executed before any of them would ever be called. Their bodies were replaced with a single throw new UnsupportedOperationException("STUBBED_FOR_POC_BUILD: ...") purely so the file compiles standalone; loadInternal() itself β€” including the vulnerable readObject() call and all of its header validation β€” is byte-for-byte unmodified from the fetched upstream source. Diff the included file against upstream master yourself to confirm; every stub is clearly marked with a STUBBED_FOR_POC_BUILD comment/message and is trivially greppable.
  4. SameDiffPoc.java crafts a malicious .sdnb file whose manifest serializes EvilGadget, a class with a readObject() override that runs id via ProcessBuilder and captures the output to a marker file β€” standing in for a real ysoserial/commons-collections gadget chain, exactly the same "prove readObject() fires unconditionally, using a controlled non-weaponized payload" methodology used for the other DL4J deserialization findings in this account.
  5. Calls the real SameDiffSerializer.load(file, false) and observes the payload's command execution.

Files:

  • SameDiffPoc.java β€” malicious harness (crafts the file, calls the real load() API).
  • SameDiffControl.java β€” negative control (crafts a .sdnb file with a legitimate, empty HashMap manifest β€” the actual expected type).
  • SameDiffSerializer_real_master_stubbed_for_standalone_build.java β€” the real upstream master source, unmodified except for the 7 unrelated stubbed methods described above (place at org/nd4j/autodiff/samediff/serde/SameDiffSerializer.java to rebuild).
  • pom.xml β€” Maven project pulling the real, unmodified, released org.nd4j:nd4j-api:1.0.0-M2.1 from Maven Central.
  • samediff_malicious_model.sdnb β€” the crafted malicious file (79 bytes).
  • run_output_malicious.txt β€” captured console output of the real run against the malicious file.
  • rce_evidence_id_output.txt β€” the marker file written by the payload's readObject(), containing the real output of id.
  • run_output_control.txt β€” captured console output of the negative control run.

Malicious file β†’ real code execution through the real load() API

$ java -cp <deps> poc.SameDiffPoc samediff_malicious_model.sdnb
[*] Crafted malicious .sdnb file: .../samediff_rce_poc.sdnb (79 bytes)
[*] Calling the REAL, unmodified org.nd4j.autodiff.samediff.serde.SameDiffSerializer.load(File, boolean)
    -- the actual public API an application uses to load a SameDiff model.

[*] load() threw (expected -- EvilGadget isn't actually the expected
    Map<String,Pair<Long,Long>> type, so the checked cast fails AFTER
    readObject() already ran and already executed the payload below): java.io.IOException: Failed to deserialize manifest from file: .../samediff_rce_poc.sdnb

Marker file written by the attacker class's readObject() during that load() call (i.e. before the wrapping IOException was ever thrown):

$ cat rce_evidence_id_output.txt
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)
RCE_VIA_SAMEDIFF_SERIALIZER_LOADINTERNAL_READOBJECT

This is the real output of the id command, spawned via ProcessBuilder from inside EvilGadget.readObject(), run by the unmodified real SameDiffSerializer.loadInternal() while processing the crafted file. The subsequent IOException/ClassCastException is irrelevant to exploitability β€” by the time it's thrown, the payload has already run.

Negative control (proves it's the malicious class, not environment/setup)

$ java -cp <deps> poc.SameDiffControl samediff_control.sdnb
[*] Crafted CONTROL .sdnb file (legit empty HashMap manifest): .../samediff_control.sdnb
[*] Calling the same real SameDiffSerializer.load(File, boolean)...
[*] load() threw: java.io.IOException: Cannot create new SameDiff instance: metadata is empty in file .../samediff_control.sdnb

With a legitimate empty HashMap manifest (the real expected type), readObject() succeeds cleanly with no side effects, no marker file is created, and execution proceeds into the real, unstubbed subsequent validation logic in loadInternal() (a genuine, unrelated "metadata is empty" check further down in the method) β€” proving the malicious run's outcome is specifically caused by the attacker-controlled class in the serialization stream, not by the harness, the header format, or the stubbed-method scaffolding.

Impact

  • Remote Code Execution: demonstrated here with a benign id-spawning payload class standing in for a weaponized gadget chain, exactly as is standard practice for CWE-502 PoCs (a real attacker substitutes a commons-collections/ysoserial-style gadget already on the target's classpath). Any application that loads an untrusted .sdnb SameDiff model file β€” a documented, intended DL4J/ND4J feature β€” is directly exposed.

Fix suggestion

  • Never call ObjectInputStream.readObject() on data derived from an untrusted file without an ObjectInputFilter allow-listing only the expected classes (java.util.HashMap, org.nd4j.common.primitives.Pair, java.lang.Long, etc.) β€” see ObjectInputFilter.Config / Validator-based filtering available since Java 9 (backport available for Java 8).
  • Alternatively, replace the ad hoc Java-serialized manifest with a length-prefixed, schema-defined format (the file already uses FlatBuffers elsewhere in this exact class β€” the manifest could trivially be encoded the same way instead of via ObjectOutputStream/ObjectInputStream).

Dedup / prior-art check performed

  • Confirmed via GitHub Security Advisories that eclipse/deeplearning4j has exactly two published advisories: GHSA-rc39-g977-687w (unrelated S3 bucket issue) and GHSA-wfhj-v5g7-vr7g / CVE-2025-53001 (ModelSerializer.loadZipData() / preprocessor.bin β€” confirmed by fetching the advisory directly β€” a different file/class/module from SameDiffSerializer).
  • Confirmed SameDiffSerializer.class is absent from every jar in the locally cached ~/.m2 repository and from the published org.nd4j:nd4j-api:1.0.0-M2.1 artifact β€” this class/bug is not covered by any existing released-artifact-based advisory or PoC.
  • Compared byte-for-byte against the sibling draft repos EnigmaConsultant/deeplearning4j-deserialize-rce (confirmed to be the already-public CVE-2025-53001/ModelSerializer.java bug β€” not filed as new here) and EnigmaConsultant/deeplearning4j-wordvector-deserialize-rce (confirmed byte-for-byte identical to the separately already-written-up EnigmaConsultant/dl4j-wordvector-deser-rce β€” not filed as new here either). Neither overlaps with this finding's class/file/module.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support