@@ -837,6 +837,46 @@ const invalid = [
837837 message : 'Cannot parse an object with a `__proto__` property'
838838 } ,
839839 {
840+ name : 'sparse array prototype pollution' ,
841+ json : '[[-7,1,"__proto__",{}]]' ,
842+ message : 'Invalid input'
843+ } ,
844+ {
845+ name : 'sparse array non-integer index' ,
846+ json : '[[-7,5,"foo",1]]' ,
847+ message : 'Invalid input'
848+ } ,
849+ {
850+ name : 'sparse array negative index' ,
851+ json : '[[-7,5,-1,1]]' ,
852+ message : 'Invalid input'
853+ } ,
854+ {
855+ name : 'sparse array out-of-bounds index' ,
856+ json : '[[-7,2,5,1]]' ,
857+ message : 'Invalid input'
858+ } ,
859+ {
860+ name : 'sparse array non-integer length' ,
861+ json : '[[-7,"abc"]]' ,
862+ message : 'Invalid input'
863+ } ,
864+ {
865+ name : 'sparse array negative length' ,
866+ json : '[[-7,-3]]' ,
867+ message : 'Invalid input'
868+ } ,
869+ {
870+ name : 'sparse array float length' ,
871+ json : '[[-7,1.5]]' ,
872+ message : 'Invalid input'
873+ } ,
874+ {
875+ name : 'sparse array float index' ,
876+ json : '[[-7,5,1.5,1]]' ,
877+ message : 'Invalid input'
878+ } ,
879+ {
840880 name : 'prototype pollution via null-prototype object' ,
841881 json : '[["null","__proto__",1],{}]' ,
842882 message : 'Cannot parse an object with a `__proto__` property'
@@ -1113,4 +1153,75 @@ uvu.test('does not create duplicate parameter names', () => {
11131153 eval ( serialized ) ;
11141154} ) ;
11151155
1156+ uvu . test ( 'rejects sparse array __proto__ pollution via parse' , ( ) => {
1157+ // Attempt to set __proto__ on an array via the sparse array encoding
1158+ const payload = JSON . stringify ( [ [ - 7 , 1 , '__proto__' , { polluted : true } ] ] ) ;
1159+ assert . throws (
1160+ ( ) => parse ( payload ) ,
1161+ ( error ) => error . message === 'Invalid input'
1162+ ) ;
1163+ } ) ;
1164+
1165+ uvu . test ( 'rejects sparse array __proto__ pollution via unflatten' , ( ) => {
1166+ // Same attack via unflatten (which receives already-parsed data)
1167+ const payload = [ [ - 7 , 1 , '__proto__' , { polluted : true } ] ] ;
1168+ assert . throws (
1169+ ( ) => unflatten ( payload ) ,
1170+ ( error ) => error . message === 'Invalid input'
1171+ ) ;
1172+ } ) ;
1173+
1174+ uvu . test ( 'sparse array CPU exhaustion payload is rejected' , ( ) => {
1175+ // Reproduction from reported vulnerability: builds deep __proto__ chains
1176+ // via sparse array encoding, causing expensive [[SetPrototypeOf]] calls.
1177+ const LAYERS = 49_000 ;
1178+ const data = [ [ - 7 , 0 ] , 0 , [ ] ] ;
1179+ for ( let i = 3 ; i < 3 + LAYERS ; i ++ ) {
1180+ data . push ( [ - 7 , 0 , '__proto__' , i - 1 ] ) ;
1181+ data [ 0 ] . push ( '__proto__' , i ) ;
1182+ }
1183+ const payload = JSON . stringify ( data ) ;
1184+
1185+ assert . throws (
1186+ ( ) => parse ( payload ) ,
1187+ ( error ) => error . message === 'Invalid input'
1188+ ) ;
1189+ } ) ;
1190+
1191+ uvu . test ( 'sparse array type confusion via __proto__ is blocked' , ( ) => {
1192+ // Reproduction from reported vulnerability: uses sparse array encoding to
1193+ // set __proto__ on an array, overwriting the prototype and allowing an
1194+ // attacker to control property values (e.g. spoofing .magnitude on a Vector).
1195+ const payload = '[[-7,0,"x",1,"y",2,"magnitude",3,"__proto__",4],3,4,"nope",["Vector",5],[6,7],8,9]' ;
1196+
1197+ class Vector {
1198+ constructor ( x , y ) {
1199+ this . x = x ;
1200+ this . y = y ;
1201+ }
1202+ get magnitude ( ) {
1203+ return ( this . x ** 2 + this . y ** 2 ) ** 0.5 ;
1204+ }
1205+ }
1206+
1207+ assert . throws (
1208+ ( ) => parse ( payload , { Vector : ( [ x , y ] ) => new Vector ( x , y ) } ) ,
1209+ ( error ) => error . message === 'Invalid input'
1210+ ) ;
1211+ } ) ;
1212+
1213+ uvu . test ( 'valid sparse array parses correctly' , ( ) => {
1214+ // Ensure the fix does not break legitimate sparse array round-tripping.
1215+ // devalue format: [root_entry, ...other_entries]
1216+ // [-7, 3, 0, 1, 2, 2] = sparse array of length 3, index 0 = entries[1], index 2 = entries[2]
1217+ const goodPayload = JSON . stringify ( [ [ - 7 , 3 , 0 , 1 , 2 , 2 ] , 'a' , 'c' ] ) ;
1218+ const result = parse ( goodPayload ) ;
1219+ assert . instance ( result , Array ) ;
1220+ assert . is ( result . length , 3 ) ;
1221+ assert . is ( result [ 0 ] , 'a' ) ;
1222+ assert . ok ( ! ( 1 in result ) ) ;
1223+ assert . is ( result [ 2 ] , 'c' ) ;
1224+ assert . is ( Object . getPrototypeOf ( result ) , Array . prototype ) ;
1225+ } ) ;
1226+
11161227uvu . test . run ( ) ;
0 commit comments