@@ -383,6 +383,71 @@ describe('binary form serializer', () => {
383383 }
384384 } ) ;
385385
386+ test ( 'rejects memory amplification attack via nested array in file offset table' , async ( ) => {
387+ // A crafted file offset table
388+ // containing a nested array like [[1e20,1e20,...,1e20]]. When file_offsets[0] is
389+ // added to a number, the inner array coerces to a ~273,000-char string. With
390+ // ~58,000 LazyFile instances from a 1MB payload, this requires ~14.7GB of memory.
391+ //
392+ // We use a small payload (13 values) that is enough to trigger the validation
393+ // error without risk of memory issues if the fix regresses.
394+ const inner_count = 13 ;
395+ const malicious_offsets = JSON . stringify ( [ [ ...Array ( inner_count ) . fill ( 1e20 ) ] ] ) ;
396+
397+ // Build a minimal binary form request with the malicious offset table
398+ const data = '[[1,3],{"file":2},["File",4],{},[5,6,7,8,9],"a.txt","text/plain",0,0,0]' ;
399+ const data_buf = text_encoder . encode ( data ) ;
400+ const offsets_buf = text_encoder . encode ( malicious_offsets ) ;
401+ const total = 7 + data_buf . length + offsets_buf . length + 1 ;
402+ const body = new Uint8Array ( total ) ;
403+ const view = new DataView ( body . buffer ) ;
404+ body [ 0 ] = 0 ;
405+ view . setUint32 ( 1 , data_buf . length , true ) ;
406+ view . setUint16 ( 5 , offsets_buf . length , true ) ;
407+ body . set ( data_buf , 7 ) ;
408+ body . set ( offsets_buf , 7 + data_buf . length ) ;
409+
410+ await expect (
411+ deserialize_binary_form (
412+ new Request ( 'http://test' , {
413+ method : 'POST' ,
414+ body,
415+ headers : {
416+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
417+ 'Content-Length' : total . toString ( )
418+ }
419+ } )
420+ )
421+ ) . rejects . toThrow ( 'invalid file offset table' ) ;
422+ } , 1000 ) ;
423+
424+ test . each ( [
425+ {
426+ name : 'nested array (amplification attack)' ,
427+ offsets : '[[1e20,1e20]]'
428+ } ,
429+ {
430+ name : 'non-integer float values' ,
431+ offsets : '[0, 1.5, 3]'
432+ } ,
433+ {
434+ name : 'negative values' ,
435+ offsets : '[0, -1, 2]'
436+ } ,
437+ {
438+ name : 'not an array (object)' ,
439+ offsets : '{"0": 0}'
440+ } ,
441+ {
442+ name : 'string values in array' ,
443+ offsets : '["0", "1"]'
444+ }
445+ ] ) ( 'rejects invalid file offset table: $name' , async ( { offsets } ) => {
446+ await expect ( deserialize_binary_form ( build_raw_request_with_offsets ( offsets ) ) ) . rejects . toThrow (
447+ 'invalid file offset table'
448+ ) ;
449+ } ) ;
450+
386451 // Regression test for https://github.com/sveltejs/kit/issues/14971
387452 test ( 'DataView offset for shared memory' , async ( ) => {
388453 const { blob } = serialize_binary_form ( { a : 1 } , { } ) ;
@@ -412,6 +477,32 @@ describe('binary form serializer', () => {
412477
413478 expect ( res . data ) . toEqual ( { a : 1 } ) ;
414479 } ) ;
480+
481+ /**
482+ * Build a binary form request with a raw devalue payload and custom file offsets JSON.
483+ * @param {string } file_offsets_json
484+ */
485+ function build_raw_request_with_offsets ( file_offsets_json ) {
486+ const devalue_data = '[[1,3],{"file":2},["File",4],{},[5,6,7,8,9],"a.txt","text/plain",0,0,0]' ;
487+ const data_buf = text_encoder . encode ( devalue_data ) ;
488+ const offsets_buf = text_encoder . encode ( file_offsets_json ) ;
489+ const total = 7 + data_buf . length + offsets_buf . length + 1 ; // +1 for a fake file byte
490+ const body = new Uint8Array ( total ) ;
491+ const view = new DataView ( body . buffer ) ;
492+ body [ 0 ] = 0 ;
493+ view . setUint32 ( 1 , data_buf . length , true ) ;
494+ view . setUint16 ( 5 , offsets_buf . length , true ) ;
495+ body . set ( data_buf , 7 ) ;
496+ body . set ( offsets_buf , 7 + data_buf . length ) ;
497+ return new Request ( 'http://test' , {
498+ method : 'POST' ,
499+ body,
500+ headers : {
501+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
502+ 'Content-Length' : total . toString ( )
503+ }
504+ } ) ;
505+ }
415506} ) ;
416507
417508describe ( 'deep_set' , ( ) => {
0 commit comments