@@ -1421,6 +1421,83 @@ uvu.test('valid sparse array parses correctly', () => {
14211421 assert . is ( Object . getPrototypeOf ( result ) , Array . prototype ) ;
14221422} ) ;
14231423
1424+ // Regression test for a DoS vulnerability in sparse array parsing.
1425+ // The SPARSE encoding is `[-7, length, idx, val, ...]`. Previously, `parse`
1426+ // handled this by calling `new Array(length)`, which V8 eagerly allocates
1427+ // a backing store for. A malicious payload containing many such arrays
1428+ // — each claiming a huge length but carrying no actual data — could force
1429+ // the parser to allocate arbitrarily large amounts of memory and crash
1430+ // the host process.
1431+ //
1432+ // Each case below crafts a payload whose combined implied allocation is
1433+ // ~20GB. With a correct fix (lazy allocation / deferred length), every
1434+ // case finishes near-instantly. Without it, the test process dies.
1435+
1436+ /**
1437+ * Builds a payload shaped like:
1438+ * [ {k0:1, k1:2, ..., k(count-1):count}, // root object, references each sparse array
1439+ * [-7, perArrayLen, 0, count+1], // sparse array #0: length = perArrayLen, index 0 -> values[count+1]
1440+ * [-7, perArrayLen, 0, count+1], // sparse array #1
1441+ * ...
1442+ * 42 ] // values[count+1], placed at index 0 of each sparse array
1443+ *
1444+ * Hydrating the root object forces every sparse array to be hydrated,
1445+ * which (without the fix) triggers `new Array(perArrayLen)` `count` times.
1446+ *
1447+ * @param {number } count
1448+ * @param {number } perArrayLen
1449+ */
1450+ function buildSparseDoSPayload ( count , perArrayLen ) {
1451+ let payload = '[{' ;
1452+
1453+ for ( let i = 0 ; i < count ; i += 1 ) {
1454+ if ( i > 0 ) payload += ',' ;
1455+ payload += `"k${ i } ":${ i + 1 } ` ;
1456+ }
1457+
1458+ payload += '}' ;
1459+
1460+ for ( let i = 0 ; i < count ; i += 1 ) {
1461+ payload += `,[${ consts . SPARSE } ,${ perArrayLen } ,0,${ count + 1 } ]` ;
1462+ }
1463+
1464+ payload += ',42]' ;
1465+ return payload ;
1466+ }
1467+
1468+ // Matrix of (perArrayLen, count) pairs — each row allocates ~2.5e9 slots
1469+ // (~20GB assuming 8-byte pointers) if the parser eagerly materializes
1470+ // sparse arrays.
1471+ const sparseDoSCases = [
1472+ { perArrayLen : 10_000 , count : 250_000 } ,
1473+ { perArrayLen : 100_000 , count : 25_000 } ,
1474+ { perArrayLen : 1_000_000 , count : 2_500 } ,
1475+ { perArrayLen : 10_000_000 , count : 250 } ,
1476+ { perArrayLen : 100_000_000 , count : 25 }
1477+ ] ;
1478+
1479+ for ( const { perArrayLen, count } of sparseDoSCases ) {
1480+ uvu . test ( `does not eagerly allocate sparse arrays (len=${ perArrayLen } , count=${ count } )` , ( ) => {
1481+ const payload = buildSparseDoSPayload ( count , perArrayLen ) ;
1482+ const result = parse ( payload ) ;
1483+
1484+ // The root is the object whose keys reference every sparse array;
1485+ // accessing them forces hydration of all `count` arrays.
1486+ assert . is ( typeof result , 'object' ) ;
1487+ assert . ok ( result !== null ) ;
1488+
1489+ // Spot-check the first and last sparse arrays.
1490+ const first = result . k0 ;
1491+ const last = result [ `k${ count - 1 } ` ] ;
1492+ assert . ok ( Array . isArray ( first ) ) ;
1493+ assert . ok ( Array . isArray ( last ) ) ;
1494+ assert . is ( first . length , perArrayLen ) ;
1495+ assert . is ( last . length , perArrayLen ) ;
1496+ assert . is ( first [ 0 ] , 42 ) ;
1497+ assert . is ( last [ 0 ] , 42 ) ;
1498+ } ) ;
1499+ }
1500+
14241501uvu . test . run ( ) ;
14251502
14261503// --- stringifyAsync tests ---
0 commit comments