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
lateston 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:
control_valid_model: a normal one-layer Dense model.numeric_extra_float32_control: the same model plus an unused largefloat32weight spec.malicious_extra_string_dos: the same model plus an unused largestringweight 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.jsonevidence/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
float32weight with shape[50000000]fails cleanly in about 15.3 ms with a tensor size mismatch. - Malicious extra
stringweight with shape[50000000]does not return before the 3 second watchdog and is killed withETIMEDOUT/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
byteLengthand 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.