@@ -124,7 +124,8 @@ describe('binary form serializer', () => {
124124 method : 'POST' ,
125125 body : blob ,
126126 headers : {
127- 'Content-Type' : BINARY_FORM_CONTENT_TYPE
127+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
128+ 'Content-Length' : blob . size . toString ( )
128129 }
129130 } )
130131 ) ;
@@ -139,7 +140,8 @@ describe('binary form serializer', () => {
139140 large : new File ( [ new Uint8Array ( 1024 ) . fill ( 'a' . charCodeAt ( 0 ) ) ] , 'large.txt' , {
140141 type : 'text/plain' ,
141142 lastModified : 100
142- } )
143+ } ) ,
144+ empty : new File ( [ ] , 'empty.txt' , { type : 'text/plain' } )
143145 } ,
144146 { }
145147 ) ;
@@ -160,11 +162,16 @@ describe('binary form serializer', () => {
160162 // @ts -expect-error duplex required in node
161163 duplex : 'half' ,
162164 headers : {
163- 'Content-Type' : BINARY_FORM_CONTENT_TYPE
165+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
166+ 'Content-Length' : blob . size . toString ( )
164167 }
165168 } )
166169 ) ;
167- const { small, large } = res . data ;
170+ const { small, large, empty } = res . data ;
171+ expect ( empty . name ) . toBe ( 'empty.txt' ) ;
172+ expect ( empty . type ) . toBe ( 'text/plain' ) ;
173+ expect ( empty . size ) . toBe ( 0 ) ;
174+ expect ( await empty . text ( ) ) . toBe ( '' ) ;
168175 expect ( small . name ) . toBe ( 'a.txt' ) ;
169176 expect ( small . type ) . toBe ( 'text/plain' ) ;
170177 expect ( small . size ) . toBe ( 1 ) ;
@@ -196,7 +203,8 @@ describe('binary form serializer', () => {
196203 method : 'POST' ,
197204 body : blob ,
198205 headers : {
199- 'Content-Type' : BINARY_FORM_CONTENT_TYPE
206+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
207+ 'Content-Length' : blob . size . toString ( )
200208 }
201209 } )
202210 ) ;
@@ -215,6 +223,166 @@ describe('binary form serializer', () => {
215223 expect ( world_slice . type ) . toBe ( file . type ) ;
216224 } ) ;
217225
226+ test ( 'throws when Content-Length is invalid' , async ( ) => {
227+ await expect (
228+ deserialize_binary_form (
229+ new Request ( 'http://test' , {
230+ method : 'POST' ,
231+ body : 'foo' ,
232+ headers : {
233+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE
234+ }
235+ } )
236+ )
237+ ) . rejects . toThrow ( 'invalid Content-Length header' ) ;
238+ await expect (
239+ deserialize_binary_form (
240+ new Request ( 'http://test' , {
241+ method : 'POST' ,
242+ body : 'foo' ,
243+ headers : {
244+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
245+ 'Content-Length' : 'invalid'
246+ }
247+ } )
248+ )
249+ ) . rejects . toThrow ( 'invalid Content-Length header' ) ;
250+ } ) ;
251+
252+ test ( 'data length check' , async ( ) => {
253+ const { blob } = serialize_binary_form (
254+ {
255+ foo : 'bar'
256+ } ,
257+ { }
258+ ) ;
259+ await expect (
260+ deserialize_binary_form (
261+ new Request ( 'http://test' , {
262+ method : 'POST' ,
263+ body : blob ,
264+ headers : {
265+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
266+ 'Content-Length' : ( blob . size - 1 ) . toString ( )
267+ }
268+ } )
269+ )
270+ ) . rejects . toThrow ( 'data overflow' ) ;
271+ } ) ;
272+
273+ test ( 'file offset table length check' , async ( ) => {
274+ const { blob } = serialize_binary_form (
275+ {
276+ file : new File ( [ '' ] , 'a.txt' )
277+ } ,
278+ { }
279+ ) ;
280+ await expect (
281+ deserialize_binary_form (
282+ new Request ( 'http://test' , {
283+ method : 'POST' ,
284+ body : blob ,
285+ headers : {
286+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
287+ 'Content-Length' : ( blob . size - 1 ) . toString ( )
288+ }
289+ } )
290+ )
291+ ) . rejects . toThrow ( 'file offset table overflow' ) ;
292+ } ) ;
293+
294+ test ( 'file length check' , async ( ) => {
295+ const { blob } = serialize_binary_form (
296+ {
297+ file : new File ( [ 'a' ] , 'a.txt' )
298+ } ,
299+ { }
300+ ) ;
301+ await expect (
302+ deserialize_binary_form (
303+ new Request ( 'http://test' , {
304+ method : 'POST' ,
305+ body : blob ,
306+ headers : {
307+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
308+ 'Content-Length' : ( blob . size - 1 ) . toString ( )
309+ }
310+ } )
311+ )
312+ ) . rejects . toThrow ( 'file data overflow' ) ;
313+ } ) ;
314+
315+ test ( 'does not preallocate large buffers for incomplete bodies' , async ( ) => {
316+ const OriginalUint8Array = Uint8Array ;
317+ const header_bytes = 1 + 4 + 2 ;
318+ const data_length = 32 * 1024 * 1024 ;
319+
320+ // This test should fail on the vulnerable implementation. To make the overallocation observable,
321+ // temporarily guard allocations of large Uint8Arrays — the fixed code only allocates after reading
322+ // the full range, so it should not trip this guard for an incomplete body.
323+ class GuardedUint8Array extends OriginalUint8Array {
324+ /** @param {...any } args */
325+ constructor ( ...args ) {
326+ if ( typeof args [ 0 ] === 'number' && args [ 0 ] === data_length ) {
327+ throw new Error ( 'EAGER_ALLOC' ) ;
328+ }
329+
330+ if ( args . length === 0 ) {
331+ super ( ) ;
332+ } else if ( args . length === 1 ) {
333+ super ( /** @type {any } */ ( args [ 0 ] ) ) ;
334+ } else if ( args . length === 2 ) {
335+ super ( /** @type {any } */ ( args [ 0 ] ) , /** @type {any } */ ( args [ 1 ] ) ) ;
336+ } else {
337+ super (
338+ /** @type {any } */ ( args [ 0 ] ) ,
339+ /** @type {any } */ ( args [ 1 ] ) ,
340+ /** @type {any } */ ( args [ 2 ] )
341+ ) ;
342+ }
343+ }
344+ }
345+
346+ /** @type {any } */ ( globalThis ) . Uint8Array = GuardedUint8Array ;
347+ try {
348+ // First chunk must include at least 1 byte past the header so that `get_buffer(header_bytes, data_length)`
349+ // takes the multi-chunk path.
350+ const first_chunk = new OriginalUint8Array ( header_bytes + 1 ) ;
351+ first_chunk [ 0 ] = 0 ;
352+ const header_view = new DataView (
353+ first_chunk . buffer ,
354+ first_chunk . byteOffset ,
355+ first_chunk . byteLength
356+ ) ;
357+ header_view . setUint32 ( 1 , data_length , true ) ;
358+ header_view . setUint16 ( 5 , 0 , true ) ;
359+
360+ const stream = new ReadableStream ( {
361+ start ( controller ) {
362+ controller . enqueue ( first_chunk ) ;
363+ controller . close ( ) ;
364+ }
365+ } ) ;
366+
367+ await expect (
368+ deserialize_binary_form (
369+ new Request ( 'http://test' , {
370+ method : 'POST' ,
371+ body : stream ,
372+ // @ts -expect-error duplex required in node
373+ duplex : 'half' ,
374+ headers : {
375+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
376+ 'Content-Length' : ( header_bytes + data_length ) . toString ( )
377+ }
378+ } )
379+ )
380+ ) . rejects . toThrow ( 'data too short' ) ;
381+ } finally {
382+ /** @type {any } */ ( globalThis ) . Uint8Array = OriginalUint8Array ;
383+ }
384+ } ) ;
385+
218386 // Regression test for https://github.com/sveltejs/kit/issues/14971
219387 test ( 'DataView offset for shared memory' , async ( ) => {
220388 const { blob } = serialize_binary_form ( { a : 1 } , { } ) ;
@@ -236,7 +404,8 @@ describe('binary form serializer', () => {
236404 // @ts -expect-error duplex required in node
237405 duplex : 'half' ,
238406 headers : {
239- 'Content-Type' : BINARY_FORM_CONTENT_TYPE
407+ 'Content-Type' : BINARY_FORM_CONTENT_TYPE ,
408+ 'Content-Length' : blob . size . toString ( )
240409 }
241410 } )
242411 ) ;
0 commit comments