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.

F-2 β€” Arbitrary file read in @tensorflow/tfjs-node.loadBinaryModel via attacker-controlled binary-manifest path (sister sink of F-1)

Authorized security research artifact disclosed via huntr.com's TensorFlow.js Model Format Vulnerability program. Source commit 7f5309fef0a47545e34049903dbdae0f97285f7e. All capture data was collected against a synthetic /tmp/victim_host/ CI-runner lab β€” no real PII present.

Real impact captured (sanitized)

Sister sink of F-1: 8 / 8 secrets recovered through a separate code path

  • Same 8 files as F-1, this time through weights[*].paths[*] (binary-format models)
  • Confirms F-1 + F-2 must be patched independently β€” one fix does not cover both

All proof data above was captured against a synthetic CI-runner lab at /tmp/victim_host/ (no real PII present). Full capture: F2_REAL_IMPACT_PROOF_2026-06-11.txt.


Summary

A Node.js service that calls tf.io.fileSystem([pbPath, manifestPath]).load() or tf.loadGraphModel(url) where url is the two-element form [pbPath, manifestPath] (used for TF1-style frozen GraphDef + separate JSON manifest) will read arbitrary files from the host filesystem and return their bytes through artifacts.weightData. The root cause is that @tensorflow/tfjs-node's NodeFileSystem.loadBinaryModel passes the attacker-controlled JSON manifest into the same loadWeights helper as the JSON-model path β€” which joins paths[] entries with path.join and reads the result via fs.readFile, with no containment check for absolute paths or .. segments.

Root Cause

Lines of Code:

In tfjs-node/src/io/file_system.ts:133-163:

protected async loadBinaryModel(): Promise<tf.io.ModelArtifacts> {
  const topologyPath        = this.path[0];                  // .pb file
  const weightManifestPath  = this.path[1];                  // JSON manifest
  // ... stat checks ...
  const modelTopology   = await readFile(this.path[0]);
  const weightsManifest = JSON.parse(await readFile(this.path[1], 'utf8'));
  const [weightSpecs, weightData] =
      await this.loadWeights(weightsManifest, this.path[1]);  // ← attacker JSON
  // ...
}

The loadWeights helper (L184-L200) iterates each weightsManifest[i].paths[j] and calls readFile(join(dirName, path)) β€” no containment. Both absolute paths (which discard dirName entirely) and .. traversals reach readFile.

Why this is NOT a duplicate of F-1: F-1 targets the one-argument entry point reached from loadJSONModel at L166-L181 β€” used by tf.loadGraphModel(url) when url is a single string and by tf.loadLayersModel(url). F-2 targets the two-argument entry point reached from loadBinaryModel at L133-L163 β€” used by tf.loadGraphModel(url) when url is [pbPath, manifestPath] (TF1-style frozen GraphDef pattern) and by direct callers of tf.io.fileSystem([pbPath, manifestPath]).load(). Different caller, different user-facing API. A maintainer patching only one will leave the other open.

Internal Pre-conditions

  1. Victim service calls one of:
    • tf.io.fileSystem([pbPath, manifestPath]).load(),
    • tf.loadGraphModel([pbPath, manifestPath]), on paths where the manifest is attacker-controlled.
  2. Process uses @tensorflow/tfjs-node (or @tensorflow/tfjs-node-gpu).

External Pre-conditions

None.

Attack Path

  1. Attacker writes any valid file (or a 1-byte placeholder) as model.pb and a malicious weights_manifest.json with paths: ['../../../../../../etc/hostname'] (or an absolute path).
  2. Attacker delivers both to the victim (model registry, upload field, etc.).
  3. Victim calls tf.io.fileSystem([pbPath, manifestPath]).load().
  4. loadBinaryModel parses the manifest and passes it to loadWeights, which reads /etc/hostname (or any other file) and stuffs its bytes into artifacts.weightData.

Impact

Arbitrary file read on the Node.js process β€” same primitive as F-1, reached through an independent entry point. Captured proof (F2_REAL_IMPACT_PROOF_2026-06-11.txt):

weightData head: ci-runner-01

(ci-runner-01 = sanitized victim_host's /etc/hostname.)

All chain-attack scenarios from F-1 apply: cloud credentials, K8s service account tokens, SSH keys, /proc/self/environ, .env files.

PoC

The repository ships a package.json so install is one step. Tested on Node 22 + @tensorflow/tfjs-node@4.22.0.

git clone https://huggingface.co/martilaio/tfjs-node-loadbinarymodel-path-traversal-poc
cd tfjs-node-loadbinarymodel-path-traversal-poc
npm install                  # pulls every dep from package.json
node reproduce.js            # minimal canary PoC β€” primitive proven
node reproduce_real_impact.js

Captured signal lands in F2_REAL_IMPACT_PROOF_2026-06-11.txt (sanitized; collected against the synthetic /tmp/victim_host/ CI-runner lab).

Mitigation

Same shared safeJoinInsideDir helper recommended for F-1 β€” applied at the single loadWeights sink closes both F-1 and F-2 simultaneously.

CVSS

CVSS 3.1 7.5 / High β€” AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N (same as F-1).

Bug classification

  • CWE-22 (Path Traversal)
  • CAPEC-126

Affected versions

@tensorflow/tfjs-node and @tensorflow/tfjs-node-gpu ≀ 4.22.0.

Files in this repository

File Purpose
README.md this disclosure
reproduce.js minimal PoC for the loadBinaryModel sink (the sister of F-1's loadJSONModel)
reproduce_real_impact.js real-impact PoC β€” recovers 8 sensitive files byte-perfect from /tmp/victim_host/
F2_REAL_IMPACT_PROOF_2026-06-11.txt sanitized captured proof from running the real-impact PoC
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