hacnho's picture
Upload folder using huggingface_hub
32810b6 verified
|
Raw
History Blame Contribute Delete
5.52 kB

TensorFlow.js model loader hangs on large string weight specs before weight validation

Summary

@tensorflow/tfjs 4.22.0 can be forced into CPU-bound processing while loading a small TensorFlow.js model file. A malicious model.json can add an unused dtype: "string" entry to weightsManifest with a very large shape, while the paired weights.bin remains tiny.

When tf.loadLayersModel() loads the model, it decodes all supplied weight specs before strict model-weight matching rejects extra weights. The string-weight decoder loops over the attacker-controlled shape and attempts to read a length prefix for every string element. With missing/truncated string data, this becomes a CPU denial-of-service before the loader can reject the malformed extra weight.

Affected Version

  • Package: @tensorflow/tfjs
  • Version tested: 4.22.0
  • npm latest on 2026-06-23: 4.22.0
  • Runtime: Node.js v20.20.2

Vulnerability Type

  • CWE-400: Uncontrolled Resource Consumption
  • CWE-835: Loop with Unreachable Exit Condition, bounded only by attacker-controlled metadata

Severity

Medium

CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H = 5.5

Proof of Concept

PoC repository:

https://huggingface.co/hacnho/tensorflowjs-string-weight-dos-poc

Run:

npm ci
npm run repro

The PoC generates and loads three TF.js model-file variants:

  1. control_valid_model: a normal one-layer Dense model.
  2. numeric_extra_float32_control: the same model plus an unused large float32 weight spec.
  3. malicious_extra_string_dos: the same model plus an unused large string weight spec.

The malicious model contains:

{
  "name": "unused_extra_string",
  "shape": [50000000],
  "dtype": "string"
}

The weights file is only 8 bytes, the same as the control Dense model weights.

Observed Results

Recorded evidence:

  • evidence/tfjs-string-weight-dos-repro-2026-06-23-132907011.json
  • evidence/tfjs-string-weight-timing-2026-06-23-132953595.json

Key results:

  • Control model loads successfully in about 10.8 ms and predicts [7].
  • Numeric extra float32 weight with shape [50000000] fails cleanly in about 15.3 ms with a tensor size mismatch.
  • Malicious extra string weight with shape [50000000] does not return before the 3 second watchdog and is killed with ETIMEDOUT / SIGKILL.

The timing curve shows linear CPU cost for string weights even when the buffer is empty:

  • shape [100000]: about 333 ms
  • shape [500000]: about 2026 ms
  • shape [1000000]: about 3272 ms

By comparison, float32, int32, and bool shape [1000000] reject in under 1 ms.

Root Cause

tf.loadLayersModel() decodes all weight specs before strict model-weight matching:

  • source/tf-layers-node-loadlayersmodel-decode-before-match.txt

The load path calls decodeModelAndOptimizerWeights(), which calls tfc.io.decodeWeights(weightData, specs) for the entire supplied spec list. Only after decoding does the loader call model.loadWeights(modelWeights, strict).

For string weights, decodeWeights() first calls getWeightBytelength(). The string branch loops from 0 to sizeFromShape(spec.shape) and reads a 4-byte length prefix for every string:

  • source/tf-core-node-decodeweights-getbytelength.txt

Then decodeWeight() has another string-specific loop over the same attacker-controlled shape:

  • source/tf-core-node-decodeweight-string-loop.txt

There is no early validation that enough bytes remain for the declared string count, and the extra unused weight is decoded before it can be rejected as not belonging to the model.

Impact

Any service or client that loads untrusted TensorFlow.js model files can be made unresponsive with a tiny model artifact:

  • model registry validators
  • model upload scanners
  • conversion services
  • notebook preprocessing jobs
  • Node.js applications using tf.loadLayersModel()
  • browser applications that load model files on the main thread

In Node.js, this blocks the worker event loop. Repeated malicious submissions can exhaust worker capacity. In browser contexts, loading on the main thread can freeze the UI.

Suggested Fix

  • Reject string weight specs when fewer than 4 bytes remain for a length prefix.
  • Reject non-finite byteLength and shape products before continuing.
  • Enforce a reasonable maximum string element count or byte budget during weight decoding.
  • Decode only weight specs that are expected by the model, or perform strict matching before decoding extra specs.
  • Ensure decodeWeightsStream() receives the same validation because it uses the same per-string loop pattern.

Duplicate Check

I checked the local submitted-report corpus, authenticated Huntr dashboard, public Huntr hacktivity, and public web search before packaging this report.

Local corpus:

  • evidence/local-duplicate-search-2026-06-23.txt
  • Result: no matches.

Authenticated Huntr dashboard:

  • evidence/dashboard-duplicate-gate-2026-06-23.json
  • 69 submitted links loaded.
  • Result: no matches.

Public Huntr hacktivity:

  • evidence/hacktivity-title-filter-results-2026-06-23.json
  • Terms: TensorFlow.js, TFJS, decodeWeights, weightsManifest, string weight, string dtype, CompositeArrayBuffer, getWeightBytelength, loadLayersModel
  • Each term scanned 1,997 public reports.
  • Result: 0 bounty links for every term.

Public web search for exact root-cause terms returned documentation/source references and unrelated usage questions, not an exact public vulnerability report.