0http-bun 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,87 @@
12
12
  * - Error message sanitization to prevent information leakage
13
13
  */
14
14
 
15
+ /**
16
+ * Symbol key for storing raw body text to prevent accidental serialization/logging (L-3 fix)
17
+ */
18
+ const RAW_BODY_SYMBOL = Symbol.for('0http.rawBody')
19
+
20
+ /**
21
+ * Keys that must be blocked to prevent prototype pollution attacks.
22
+ * Shared across all parsers (URL-encoded, multipart, nested key parsing).
23
+ */
24
+ const PROTOTYPE_POLLUTION_KEYS = new Set([
25
+ '__proto__',
26
+ 'constructor',
27
+ 'prototype',
28
+ 'hasOwnProperty',
29
+ 'isPrototypeOf',
30
+ 'propertyIsEnumerable',
31
+ 'valueOf',
32
+ 'toString',
33
+ ])
34
+
35
+ /**
36
+ * Reads request body as text with inline size enforcement.
37
+ * Streams the body and aborts if the accumulated size exceeds the limit,
38
+ * preventing full memory allocation for oversized payloads (H-3 fix).
39
+ * @param {Request} req - Request object
40
+ * @param {number} maxBytes - Maximum allowed body size in bytes
41
+ * @returns {Promise<string>} Body text
42
+ * @throws {Object} Object with `status` and `message` on limit exceeded
43
+ */
44
+ async function readBodyWithLimit(req, maxBytes) {
45
+ // If the request has no body stream, fall back to req.text()
46
+ if (!req.body || typeof req.body.getReader !== 'function') {
47
+ const text = await req.text()
48
+ const byteLength = new TextEncoder().encode(text).length
49
+ if (byteLength > maxBytes) {
50
+ const err = new Error('Request body size exceeded')
51
+ err.status = 413
52
+ throw err
53
+ }
54
+ return text
55
+ }
56
+
57
+ const reader = req.body.getReader()
58
+ const chunks = []
59
+ let totalBytes = 0
60
+
61
+ try {
62
+ while (true) {
63
+ const {done, value} = await reader.read()
64
+ if (done) break
65
+
66
+ totalBytes += value.byteLength
67
+ if (totalBytes > maxBytes) {
68
+ reader.cancel()
69
+ const err = new Error('Request body size exceeded')
70
+ err.status = 413
71
+ throw err
72
+ }
73
+ chunks.push(value)
74
+ }
75
+ } catch (e) {
76
+ // Re-throw size limit errors
77
+ if (e.status === 413) throw e
78
+ // Wrap other stream errors
79
+ const err = new Error('Failed to read request body')
80
+ err.status = 400
81
+ throw err
82
+ }
83
+
84
+ // Concatenate chunks and decode to string
85
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0)
86
+ const combined = new Uint8Array(totalLength)
87
+ let offset = 0
88
+ for (const chunk of chunks) {
89
+ combined.set(chunk, offset)
90
+ offset += chunk.byteLength
91
+ }
92
+
93
+ return new TextDecoder().decode(combined)
94
+ }
95
+
15
96
  /**
16
97
  * Parses size limit strings with suffixes (e.g. '500b', '1kb', '2mb')
17
98
  * @param {number|string} limit - Size limit
@@ -64,7 +145,9 @@ function parseLimit(limit) {
64
145
  return Math.min(bytes, 1024 * 1024 * 1024) // Max 1GB
65
146
  }
66
147
 
67
- return limit || 1024 * 1024 // Default 1MB
148
+ throw new TypeError(
149
+ `Invalid limit type: expected number or string, got ${typeof limit}`,
150
+ )
68
151
  }
69
152
 
70
153
  /**
@@ -113,19 +196,7 @@ function parseNestedKey(obj, key, value, depth = 0) {
113
196
  throw new Error('Maximum nesting depth exceeded')
114
197
  }
115
198
 
116
- // Protect against prototype pollution
117
- const prototypePollutionKeys = [
118
- '__proto__',
119
- 'constructor',
120
- 'prototype',
121
- 'hasOwnProperty',
122
- 'isPrototypeOf',
123
- 'propertyIsEnumerable',
124
- 'valueOf',
125
- 'toString',
126
- ]
127
-
128
- if (prototypePollutionKeys.includes(key)) {
199
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) {
129
200
  return // Silently ignore dangerous keys
130
201
  }
131
202
 
@@ -138,7 +209,7 @@ function parseNestedKey(obj, key, value, depth = 0) {
138
209
  const [, baseKey, indexKey, remaining] = match
139
210
 
140
211
  // Protect against prototype pollution on base key
141
- if (prototypePollutionKeys.includes(baseKey)) {
212
+ if (PROTOTYPE_POLLUTION_KEYS.has(baseKey)) {
142
213
  return
143
214
  }
144
215
 
@@ -161,7 +232,7 @@ function parseNestedKey(obj, key, value, depth = 0) {
161
232
  }
162
233
  } else {
163
234
  // Protect against prototype pollution on index key
164
- if (!prototypePollutionKeys.includes(indexKey)) {
235
+ if (!PROTOTYPE_POLLUTION_KEYS.has(indexKey)) {
165
236
  obj[baseKey][indexKey] = value
166
237
  }
167
238
  }
@@ -194,90 +265,104 @@ function createJSONParser(options = {}) {
194
265
  return deferNext ? null : next()
195
266
  }
196
267
 
197
- try {
198
- const contentLength = req.headers.get('content-length')
199
- if (contentLength) {
200
- const length = parseInt(contentLength)
201
- if (isNaN(length) || length < 0) {
202
- return new Response('Invalid content-length header', {status: 400})
203
- }
204
- if (length > parsedLimit) {
205
- return new Response('Request body size exceeded', {status: 413})
206
- }
268
+ const contentLength = req.headers.get('content-length')
269
+ if (contentLength) {
270
+ const length = parseInt(contentLength)
271
+ if (isNaN(length) || length < 0) {
272
+ return new Response('Invalid content-length header', {status: 400})
207
273
  }
208
-
209
- // Check if the request has a null body (no body was provided)
210
- if (req.body === null) {
211
- Object.defineProperty(req, 'body', {
212
- value: undefined,
213
- writable: true,
214
- enumerable: true,
215
- configurable: true,
216
- })
217
- return deferNext ? null : next()
274
+ if (length > parsedLimit) {
275
+ return new Response('Request body size exceeded', {status: 413})
218
276
  }
277
+ }
219
278
 
220
- const text = await req.text()
221
- // Store raw body text for verification
222
- req._rawBodyText = text
279
+ // Check if the request has a null body (no body was provided)
280
+ if (req.body === null) {
281
+ Object.defineProperty(req, 'body', {
282
+ value: undefined,
283
+ writable: true,
284
+ enumerable: true,
285
+ configurable: true,
286
+ })
287
+ return deferNext ? null : next()
288
+ }
223
289
 
224
- // Validate text length to prevent memory exhaustion
225
- const textLength = new TextEncoder().encode(text).length
226
- if (textLength > parsedLimit) {
290
+ let text
291
+ try {
292
+ text = await readBodyWithLimit(req, parsedLimit)
293
+ } catch (e) {
294
+ if (e.status === 413) {
227
295
  return new Response('Request body size exceeded', {status: 413})
228
296
  }
229
-
230
- // Additional protection against excessively deep nesting
231
- if (text.length > 0) {
232
- // Count nesting levels to prevent stack overflow during parsing
233
- let nestingLevel = 0
234
- let maxNesting = 0
235
- for (let i = 0; i < text.length; i++) {
236
- if (text[i] === '{' || text[i] === '[') {
237
- nestingLevel++
238
- maxNesting = Math.max(maxNesting, nestingLevel)
239
- } else if (text[i] === '}' || text[i] === ']') {
240
- nestingLevel--
241
- }
297
+ return new Response('Failed to read request body', {status: 400})
298
+ }
299
+ // Store raw body text for verification (L-3: use Symbol to prevent accidental serialization)
300
+ req[RAW_BODY_SYMBOL] = text
301
+
302
+ // Additional protection against excessively deep nesting
303
+ if (text.length > 0) {
304
+ // Count nesting levels with string-awareness to prevent bypass via string content
305
+ let nestingLevel = 0
306
+ let maxNesting = 0
307
+ let inString = false
308
+ let escape = false
309
+ for (let i = 0; i < text.length; i++) {
310
+ const ch = text[i]
311
+ if (escape) {
312
+ escape = false
313
+ continue
242
314
  }
243
- if (maxNesting > 100) {
244
- return new Response('JSON nesting too deep', {status: 400})
315
+ if (ch === '\\' && inString) {
316
+ escape = true
317
+ continue
318
+ }
319
+ if (ch === '"') {
320
+ inString = !inString
321
+ continue
322
+ }
323
+ if (inString) continue
324
+ if (ch === '{' || ch === '[') {
325
+ nestingLevel++
326
+ maxNesting = Math.max(maxNesting, nestingLevel)
327
+ } else if (ch === '}' || ch === ']') {
328
+ nestingLevel--
245
329
  }
246
330
  }
247
-
248
- // Handle empty string body (becomes empty object)
249
- if (text === '' || text.trim() === '') {
250
- Object.defineProperty(req, 'body', {
251
- value: {},
252
- writable: true,
253
- enumerable: true,
254
- configurable: true,
255
- })
256
- return deferNext ? null : next()
257
- }
258
-
259
- let body
260
- try {
261
- body = JSON.parse(text, reviver)
262
- } catch (parseError) {
263
- throw new Error(`Invalid JSON: ${parseError.message}`)
264
- }
265
-
266
- if (strict && typeof body !== 'object') {
267
- throw new Error('JSON body must be an object or array')
331
+ if (maxNesting > 100) {
332
+ return new Response('JSON nesting too deep', {status: 400})
268
333
  }
334
+ }
269
335
 
336
+ // Handle empty string body — return undefined to distinguish from actual data (L-2 fix)
337
+ if (text === '' || text.trim() === '') {
270
338
  Object.defineProperty(req, 'body', {
271
- value: body,
339
+ value: undefined,
272
340
  writable: true,
273
341
  enumerable: true,
274
342
  configurable: true,
275
343
  })
276
-
277
344
  return deferNext ? null : next()
278
- } catch (error) {
279
- throw error
280
345
  }
346
+
347
+ let body
348
+ try {
349
+ body = JSON.parse(text, reviver)
350
+ } catch (parseError) {
351
+ throw new Error(`Invalid JSON: ${parseError.message}`)
352
+ }
353
+
354
+ if (strict && typeof body !== 'object') {
355
+ throw new Error('JSON body must be an object or array')
356
+ }
357
+
358
+ Object.defineProperty(req, 'body', {
359
+ value: body,
360
+ writable: true,
361
+ enumerable: true,
362
+ configurable: true,
363
+ })
364
+
365
+ return deferNext ? null : next()
281
366
  }
282
367
  }
283
368
 
@@ -299,38 +384,37 @@ function createTextParser(options = {}) {
299
384
  return deferNext ? null : next()
300
385
  }
301
386
 
302
- try {
303
- const contentLength = req.headers.get('content-length')
304
- if (contentLength) {
305
- const length = parseInt(contentLength)
306
- if (isNaN(length) || length < 0) {
307
- return new Response('Invalid content-length header', {status: 400})
308
- }
309
- if (length > parsedLimit) {
310
- return new Response('Request body size exceeded', {status: 413})
311
- }
387
+ const contentLength = req.headers.get('content-length')
388
+ if (contentLength) {
389
+ const length = parseInt(contentLength)
390
+ if (isNaN(length) || length < 0) {
391
+ return new Response('Invalid content-length header', {status: 400})
312
392
  }
393
+ if (length > parsedLimit) {
394
+ return new Response('Request body size exceeded', {status: 413})
395
+ }
396
+ }
313
397
 
314
- const text = await req.text()
315
- // Store raw body text for verification
316
- req._rawBodyText = text
317
-
318
- const textLength = new TextEncoder().encode(text).length
319
- if (textLength > parsedLimit) {
398
+ let text
399
+ try {
400
+ text = await readBodyWithLimit(req, parsedLimit)
401
+ } catch (e) {
402
+ if (e.status === 413) {
320
403
  return new Response('Request body size exceeded', {status: 413})
321
404
  }
405
+ return new Response('Failed to read request body', {status: 400})
406
+ }
407
+ // Store raw body text for verification (L-3: use Symbol to prevent accidental serialization)
408
+ req[RAW_BODY_SYMBOL] = text
322
409
 
323
- Object.defineProperty(req, 'body', {
324
- value: text,
325
- writable: true,
326
- enumerable: true,
327
- configurable: true,
328
- })
410
+ Object.defineProperty(req, 'body', {
411
+ value: text,
412
+ writable: true,
413
+ enumerable: true,
414
+ configurable: true,
415
+ })
329
416
 
330
- return deferNext ? null : next()
331
- } catch (error) {
332
- throw error
333
- }
417
+ return deferNext ? null : next()
334
418
  }
335
419
  }
336
420
 
@@ -338,8 +422,8 @@ function createTextParser(options = {}) {
338
422
  * Creates a URL-encoded form parser middleware
339
423
  * @param {Object} options - Body parser configuration
340
424
  * @param {number|string} options.limit - Maximum body size in bytes
341
- * @param {boolean} options.extended - Use extended query string parsing
342
- * @param {boolean} options.parseNestedObjects - Parse nested object notation
425
+ * @param {boolean} options.extended - Enable rich parsing: nested objects, arrays, and duplicate key merging (default: true). When false, only flat key-value pairs are returned.
426
+ * @param {boolean} options.parseNestedObjects - Parse bracket notation (e.g. a[b]=1) into nested objects. Only applies when extended=true (default: true)
343
427
  * @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
344
428
  * @returns {Function} Middleware function
345
429
  */
@@ -361,92 +445,87 @@ function createURLEncodedParser(options = {}) {
361
445
  return deferNext ? null : next()
362
446
  }
363
447
 
364
- try {
365
- const contentLength = req.headers.get('content-length')
366
- if (contentLength) {
367
- const length = parseInt(contentLength)
368
- if (isNaN(length) || length < 0) {
369
- return new Response('Invalid content-length header', {status: 400})
370
- }
371
- if (length > parsedLimit) {
372
- return new Response('Request body size exceeded', {status: 413})
373
- }
448
+ const contentLength = req.headers.get('content-length')
449
+ if (contentLength) {
450
+ const length = parseInt(contentLength)
451
+ if (isNaN(length) || length < 0) {
452
+ return new Response('Invalid content-length header', {status: 400})
374
453
  }
454
+ if (length > parsedLimit) {
455
+ return new Response('Request body size exceeded', {status: 413})
456
+ }
457
+ }
375
458
 
376
- const text = await req.text()
377
- // Store raw body text for verification
378
- req._rawBodyText = text
379
-
380
- const textLength = new TextEncoder().encode(text).length
381
- if (textLength > parsedLimit) {
459
+ let text
460
+ try {
461
+ text = await readBodyWithLimit(req, parsedLimit)
462
+ } catch (e) {
463
+ if (e.status === 413) {
382
464
  return new Response('Request body size exceeded', {status: 413})
383
465
  }
466
+ return new Response('Failed to read request body', {status: 400})
467
+ }
468
+ // Store raw body text for verification (L-3: use Symbol to prevent accidental serialization)
469
+ req[RAW_BODY_SYMBOL] = text
384
470
 
385
- const body = {}
386
- const params = new URLSearchParams(text)
471
+ const body = Object.create(null)
472
+ const params = new URLSearchParams(text)
387
473
 
388
- // Prevent DoS through excessive parameters
389
- let paramCount = 0
390
- const maxParams = 1000 // Reasonable limit for URL-encoded parameters
474
+ // Prevent DoS through excessive parameters
475
+ let paramCount = 0
476
+ const maxParams = 1000 // Reasonable limit for URL-encoded parameters
391
477
 
392
- for (const [key, value] of params.entries()) {
393
- paramCount++
394
- if (paramCount > maxParams) {
395
- return new Response('Too many parameters', {status: 400})
396
- }
478
+ for (const [key, value] of params.entries()) {
479
+ paramCount++
480
+ if (paramCount > maxParams) {
481
+ return new Response('Too many parameters', {status: 400})
482
+ }
397
483
 
398
- // Validate key and value lengths to prevent memory exhaustion
399
- if (key.length > 1000 || value.length > 10000) {
400
- return new Response('Parameter too long', {status: 400})
401
- }
484
+ // Validate key and value lengths to prevent memory exhaustion
485
+ if (key.length > 1000 || value.length > 10000) {
486
+ return new Response('Parameter too long', {status: 400})
487
+ }
402
488
 
403
- if (parseNestedObjects) {
404
- try {
405
- parseNestedKey(body, key, value)
406
- } catch (parseError) {
407
- return new Response(
408
- `Invalid parameter structure: ${parseError.message}`,
409
- {status: 400},
410
- )
411
- }
412
- } else {
413
- // Protect against prototype pollution even when parseNestedObjects is false
414
- const prototypePollutionKeys = [
415
- '__proto__',
416
- 'constructor',
417
- 'prototype',
418
- 'hasOwnProperty',
419
- 'isPrototypeOf',
420
- 'propertyIsEnumerable',
421
- 'valueOf',
422
- 'toString',
423
- ]
424
-
425
- if (!prototypePollutionKeys.includes(key)) {
426
- if (body[key] !== undefined) {
427
- if (Array.isArray(body[key])) {
428
- body[key].push(value)
429
- } else {
430
- body[key] = [body[key], value]
431
- }
489
+ if (extended && parseNestedObjects) {
490
+ // Extended + nested: parse bracket notation into nested objects/arrays
491
+ // (parseNestedKey has its own prototype pollution guards via PROTOTYPE_POLLUTION_KEYS)
492
+ try {
493
+ parseNestedKey(body, key, value)
494
+ } catch (parseError) {
495
+ return new Response(
496
+ `Invalid parameter structure: ${parseError.message}`,
497
+ {status: 400},
498
+ )
499
+ }
500
+ } else if (extended) {
501
+ // Extended but no nested parsing: flat keys with duplicate key merging into arrays
502
+ if (!PROTOTYPE_POLLUTION_KEYS.has(key)) {
503
+ if (body[key] !== undefined) {
504
+ if (Array.isArray(body[key])) {
505
+ body[key].push(value)
432
506
  } else {
433
- body[key] = value
507
+ body[key] = [body[key], value]
434
508
  }
509
+ } else {
510
+ body[key] = value
435
511
  }
436
512
  }
513
+ } else {
514
+ // Simple mode: flat key-value pairs, last value wins
515
+ if (!PROTOTYPE_POLLUTION_KEYS.has(key)) {
516
+ body[key] = value
517
+ }
437
518
  }
519
+ }
438
520
 
439
- Object.defineProperty(req, 'body', {
440
- value: body,
441
- writable: true,
442
- enumerable: true,
443
- configurable: true,
444
- })
521
+ Object.defineProperty(req, 'body', {
522
+ value: body,
523
+ writable: true,
524
+ enumerable: true,
525
+ configurable: true,
526
+ })
445
527
 
446
- return deferNext ? null : next()
447
- } catch (error) {
448
- throw error
449
- }
528
+ return deferNext ? null : next()
450
529
  }
451
530
  }
452
531
 
@@ -468,109 +547,141 @@ function createMultipartParser(options = {}) {
468
547
  return deferNext ? null : next()
469
548
  }
470
549
 
471
- try {
472
- const contentLength = req.headers.get('content-length')
473
- if (contentLength) {
474
- const length = parseInt(contentLength)
475
- if (isNaN(length) || length < 0) {
476
- return new Response('Invalid content-length header', {status: 400})
477
- }
478
- if (length > parsedLimit) {
479
- return new Response('Request body size exceeded', {status: 413})
480
- }
550
+ const contentLength = req.headers.get('content-length')
551
+ if (contentLength) {
552
+ const length = parseInt(contentLength)
553
+ if (isNaN(length) || length < 0) {
554
+ return new Response('Invalid content-length header', {status: 400})
555
+ }
556
+ if (length > parsedLimit) {
557
+ return new Response('Request body size exceeded', {status: 413})
481
558
  }
559
+ }
482
560
 
483
- const formData = await req.formData()
561
+ const formData = await req.formData()
484
562
 
485
- // Calculate actual size of form data and validate
486
- let totalSize = 0
487
- let fieldCount = 0
488
- const maxFields = 100 // Reasonable limit for form fields
563
+ // Single-pass: validate and extract simultaneously (H-5 fix)
564
+ // Merges the validation and extraction loops to avoid double iteration
565
+ let totalSize = 0
566
+ let fieldCount = 0
567
+ const maxFields = 100 // Reasonable limit for form fields
489
568
 
490
- for (const [key, value] of formData.entries()) {
491
- fieldCount++
492
- if (fieldCount > maxFields) {
493
- return new Response('Too many form fields', {status: 400})
494
- }
569
+ const body = Object.create(null)
570
+ const files = Object.create(null)
495
571
 
496
- // Validate field name length
497
- if (key.length > 1000) {
498
- return new Response('Field name too long', {status: 400})
499
- }
572
+ for (const [key, value] of formData.entries()) {
573
+ fieldCount++
574
+ if (fieldCount > maxFields) {
575
+ return new Response('Too many form fields', {status: 400})
576
+ }
500
577
 
501
- if (value instanceof File) {
502
- totalSize += value.size
503
- // Validate file name length for security
504
- if (value.name && value.name.length > 255) {
505
- return new Response('Filename too long', {status: 400})
506
- }
507
- // Validate file size individually
508
- if (value.size > parsedLimit) {
509
- return new Response('File too large', {status: 413})
510
- }
511
- } else {
512
- const valueSize = new TextEncoder().encode(value).length
513
- totalSize += valueSize
514
- // Validate field value length
515
- if (valueSize > 100000) {
516
- // 100KB per field
517
- return new Response('Field value too long', {status: 400})
518
- }
578
+ // Validate field name length
579
+ if (key.length > 1000) {
580
+ return new Response('Field name too long', {status: 400})
581
+ }
582
+
583
+ // Skip prototype pollution keys
584
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) {
585
+ continue
586
+ }
587
+
588
+ if (value instanceof File) {
589
+ totalSize += value.size
590
+ // Validate file name length for security
591
+ if (value.name && value.name.length > 255) {
592
+ return new Response('Filename too long', {status: 400})
519
593
  }
594
+ // Validate file size individually
595
+ if (value.size > parsedLimit) {
596
+ return new Response('File too large', {status: 413})
597
+ }
598
+
520
599
  totalSize += new TextEncoder().encode(key).length
521
600
 
522
- // Check total size periodically to prevent memory exhaustion
601
+ // Check total size to prevent memory exhaustion
523
602
  if (totalSize > parsedLimit) {
524
603
  return new Response('Request body size exceeded', {status: 413})
525
604
  }
526
- }
527
605
 
528
- const body = {}
529
- const files = {}
530
-
531
- for (const [key, value] of formData.entries()) {
532
- if (value instanceof File) {
533
- const mimetype = value.type?.split(';')[0] || value.type
534
- const fileData = new Uint8Array(await value.arrayBuffer())
606
+ const mimetype = value.type?.split(';')[0] || value.type
607
+ const fileData = new Uint8Array(await value.arrayBuffer())
608
+ // Sanitize filename to prevent path traversal
609
+ let sanitizedFilename = value.name
610
+ if (sanitizedFilename) {
611
+ const originalName = sanitizedFilename
612
+ // Remove null bytes
613
+ sanitizedFilename = sanitizedFilename.replace(/\0/g, '')
614
+ // Remove path separators and directory components
615
+ sanitizedFilename = sanitizedFilename.replace(/\.\./g, '')
616
+ sanitizedFilename = sanitizedFilename.replace(/[/\\]/g, '')
617
+ // Remove leading dots (hidden files)
618
+ sanitizedFilename = sanitizedFilename.replace(/^\.+/, '')
619
+ // If nothing is left after sanitization, use a default name
620
+ if (!sanitizedFilename) {
621
+ sanitizedFilename = 'upload'
622
+ }
535
623
  files[key] = {
536
- filename: value.name,
537
- name: value.name,
624
+ filename: sanitizedFilename,
625
+ originalName: originalName,
626
+ name: sanitizedFilename,
538
627
  size: value.size,
539
628
  type: value.type,
540
629
  mimetype: mimetype,
541
630
  data: fileData,
542
631
  }
543
632
  } else {
544
- if (body[key] !== undefined) {
545
- if (Array.isArray(body[key])) {
546
- body[key].push(value)
547
- } else {
548
- body[key] = [body[key], value]
549
- }
550
- } else {
551
- body[key] = value
633
+ files[key] = {
634
+ filename: value.name,
635
+ name: value.name,
636
+ size: value.size,
637
+ type: value.type,
638
+ mimetype: mimetype,
639
+ data: fileData,
552
640
  }
553
641
  }
554
- }
642
+ } else {
643
+ const valueSize = new TextEncoder().encode(value).length
644
+ totalSize += valueSize
645
+ // Validate field value length
646
+ if (valueSize > 100000) {
647
+ // 100KB per field
648
+ return new Response('Field value too long', {status: 400})
649
+ }
555
650
 
556
- Object.defineProperty(req, 'body', {
557
- value: body,
558
- writable: true,
559
- enumerable: true,
560
- configurable: true,
561
- })
651
+ totalSize += new TextEncoder().encode(key).length
562
652
 
563
- Object.defineProperty(req, 'files', {
564
- value: files,
565
- writable: true,
566
- enumerable: true,
567
- configurable: true,
568
- })
653
+ // Check total size to prevent memory exhaustion
654
+ if (totalSize > parsedLimit) {
655
+ return new Response('Request body size exceeded', {status: 413})
656
+ }
569
657
 
570
- return deferNext ? null : next()
571
- } catch (error) {
572
- throw error
658
+ if (body[key] !== undefined) {
659
+ if (Array.isArray(body[key])) {
660
+ body[key].push(value)
661
+ } else {
662
+ body[key] = [body[key], value]
663
+ }
664
+ } else {
665
+ body[key] = value
666
+ }
667
+ }
573
668
  }
669
+
670
+ Object.defineProperty(req, 'body', {
671
+ value: body,
672
+ writable: true,
673
+ enumerable: true,
674
+ configurable: true,
675
+ })
676
+
677
+ Object.defineProperty(req, 'files', {
678
+ value: files,
679
+ writable: true,
680
+ enumerable: true,
681
+ configurable: true,
682
+ })
683
+
684
+ return deferNext ? null : next()
574
685
  }
575
686
  }
576
687
 
@@ -586,6 +697,7 @@ function createMultipartParser(options = {}) {
586
697
  * @param {Function} options.onError - Custom error handler
587
698
  * @param {Function} options.verify - Body verification function
588
699
  * @param {boolean} options.parseNestedObjects - Parse nested object notation (for compatibility)
700
+ * @param {boolean} options.extended - Enable rich URL-encoded parsing (for compatibility, forwarded to urlencoded.extended)
589
701
  * @param {string|number} options.jsonLimit - JSON size limit (backward compatibility)
590
702
  * @param {string|number} options.textLimit - Text size limit (backward compatibility)
591
703
  * @param {string|number} options.urlencodedLimit - URL-encoded size limit (backward compatibility)
@@ -603,6 +715,7 @@ function createBodyParser(options = {}) {
603
715
  onError,
604
716
  verify,
605
717
  parseNestedObjects = true,
718
+ extended,
606
719
  // Backward compatibility for direct limit options
607
720
  jsonLimit,
608
721
  textLimit,
@@ -626,6 +739,12 @@ function createBodyParser(options = {}) {
626
739
  urlencoded.urlencodedLimit ||
627
740
  urlencoded.limit ||
628
741
  '1mb',
742
+ extended:
743
+ urlencoded.extended !== undefined
744
+ ? urlencoded.extended
745
+ : extended !== undefined
746
+ ? extended
747
+ : true,
629
748
  parseNestedObjects:
630
749
  urlencoded.parseNestedObjects !== undefined
631
750
  ? urlencoded.parseNestedObjects
@@ -640,7 +759,7 @@ function createBodyParser(options = {}) {
640
759
  // Create parsers with custom types consideration
641
760
  const jsonParserMiddleware = createJSONParser({
642
761
  ...jsonOptions,
643
- type: 'application/', // Broad match for JSON types
762
+ type: 'application/json', // Match only JSON content types
644
763
  deferNext: !!verify, // Defer next if verification is enabled
645
764
  })
646
765
  const textParserMiddleware = createTextParser({
@@ -686,7 +805,26 @@ function createBodyParser(options = {}) {
686
805
 
687
806
  // Custom JSON parser handling for custom JSON types (case-insensitive)
688
807
  if (jsonParser && isJsonType(contentType, jsonTypes)) {
689
- const text = await req.text()
808
+ const contentLength = req.headers.get('content-length')
809
+ const jsonParsedLimit = parseLimit(jsonOptions.limit)
810
+ if (contentLength) {
811
+ const length = parseInt(contentLength)
812
+ if (isNaN(length) || length < 0) {
813
+ return new Response('Invalid content-length header', {status: 400})
814
+ }
815
+ if (length > jsonParsedLimit) {
816
+ return new Response('Request body size exceeded', {status: 413})
817
+ }
818
+ }
819
+ let text
820
+ try {
821
+ text = await readBodyWithLimit(req, jsonParsedLimit)
822
+ } catch (e) {
823
+ if (e.status === 413) {
824
+ return new Response('Request body size exceeded', {status: 413})
825
+ }
826
+ return new Response('Failed to read request body', {status: 400})
827
+ }
690
828
  const body = jsonParser(text)
691
829
  Object.defineProperty(req, 'body', {
692
830
  value: body,
@@ -699,7 +837,15 @@ function createBodyParser(options = {}) {
699
837
  } else {
700
838
  // Check if content type matches any JSON types first (including custom ones)
701
839
  if (isJsonType(contentType, jsonTypes)) {
702
- result = await jsonParserMiddleware(req, next)
840
+ // Use a JSON parser that matches the detected custom type
841
+ const customTypeJsonParser = createJSONParser({
842
+ ...jsonOptions,
843
+ type: jsonTypes.find((t) =>
844
+ contentType.toLowerCase().includes(t.toLowerCase()),
845
+ ),
846
+ deferNext: !!verify,
847
+ })
848
+ result = await customTypeJsonParser(req, next)
703
849
  } else {
704
850
  // Route to appropriate parser based on content type (case-insensitive)
705
851
  const lowerContentType = contentType.toLowerCase()
@@ -735,10 +881,10 @@ function createBodyParser(options = {}) {
735
881
  if (verify && req.body !== undefined) {
736
882
  try {
737
883
  // For verification, we need to pass the raw body text
738
- // Get the original text/data that was parsed
884
+ // Get the original text/data that was parsed (L-3: read from Symbol)
739
885
  let rawBody = ''
740
- if (req._rawBodyText) {
741
- rawBody = req._rawBodyText
886
+ if (req[RAW_BODY_SYMBOL]) {
887
+ rawBody = req[RAW_BODY_SYMBOL]
742
888
  }
743
889
  verify(req, rawBody)
744
890
  } catch (verifyError) {
@@ -777,6 +923,7 @@ module.exports = {
777
923
  hasBody,
778
924
  shouldParse,
779
925
  parseLimit,
926
+ RAW_BODY_SYMBOL,
780
927
  }
781
928
 
782
929
  // Default export is the main body parser