1717 */
1818const RAW_BODY_SYMBOL = Symbol . for ( '0http.rawBody' )
1919
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+
2035/**
2136 * Reads request body as text with inline size enforcement.
2237 * Streams the body and aborts if the accumulated size exceeds the limit,
@@ -181,19 +196,7 @@ function parseNestedKey(obj, key, value, depth = 0) {
181196 throw new Error ( 'Maximum nesting depth exceeded' )
182197 }
183198
184- // Protect against prototype pollution
185- const prototypePollutionKeys = [
186- '__proto__' ,
187- 'constructor' ,
188- 'prototype' ,
189- 'hasOwnProperty' ,
190- 'isPrototypeOf' ,
191- 'propertyIsEnumerable' ,
192- 'valueOf' ,
193- 'toString' ,
194- ]
195-
196- if ( prototypePollutionKeys . includes ( key ) ) {
199+ if ( PROTOTYPE_POLLUTION_KEYS . has ( key ) ) {
197200 return // Silently ignore dangerous keys
198201 }
199202
@@ -206,7 +209,7 @@ function parseNestedKey(obj, key, value, depth = 0) {
206209 const [ , baseKey , indexKey , remaining ] = match
207210
208211 // Protect against prototype pollution on base key
209- if ( prototypePollutionKeys . includes ( baseKey ) ) {
212+ if ( PROTOTYPE_POLLUTION_KEYS . has ( baseKey ) ) {
210213 return
211214 }
212215
@@ -229,7 +232,7 @@ function parseNestedKey(obj, key, value, depth = 0) {
229232 }
230233 } else {
231234 // Protect against prototype pollution on index key
232- if ( ! prototypePollutionKeys . includes ( indexKey ) ) {
235+ if ( ! PROTOTYPE_POLLUTION_KEYS . has ( indexKey ) ) {
233236 obj [ baseKey ] [ indexKey ] = value
234237 }
235238 }
@@ -419,8 +422,8 @@ function createTextParser(options = {}) {
419422 * Creates a URL-encoded form parser middleware
420423 * @param {Object } options - Body parser configuration
421424 * @param {number|string } options.limit - Maximum body size in bytes
422- * @param {boolean } options.extended - Use extended query string parsing
423- * @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)
424427 * @param {boolean } options.deferNext - If true, don't call next() and let caller handle it
425428 * @returns {Function } Middleware function
426429 */
@@ -465,7 +468,7 @@ function createURLEncodedParser(options = {}) {
465468 // Store raw body text for verification (L-3: use Symbol to prevent accidental serialization)
466469 req [ RAW_BODY_SYMBOL ] = text
467470
468- const body = { }
471+ const body = Object . create ( null )
469472 const params = new URLSearchParams ( text )
470473
471474 // Prevent DoS through excessive parameters
@@ -483,7 +486,9 @@ function createURLEncodedParser(options = {}) {
483486 return new Response ( 'Parameter too long' , { status : 400 } )
484487 }
485488
486- if ( parseNestedObjects ) {
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)
487492 try {
488493 parseNestedKey ( body , key , value )
489494 } catch ( parseError ) {
@@ -492,20 +497,9 @@ function createURLEncodedParser(options = {}) {
492497 { status : 400 } ,
493498 )
494499 }
495- } else {
496- // Protect against prototype pollution even when parseNestedObjects is false
497- const prototypePollutionKeys = [
498- '__proto__' ,
499- 'constructor' ,
500- 'prototype' ,
501- 'hasOwnProperty' ,
502- 'isPrototypeOf' ,
503- 'propertyIsEnumerable' ,
504- 'valueOf' ,
505- 'toString' ,
506- ]
507-
508- if ( ! prototypePollutionKeys . includes ( key ) ) {
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 ) ) {
509503 if ( body [ key ] !== undefined ) {
510504 if ( Array . isArray ( body [ key ] ) ) {
511505 body [ key ] . push ( value )
@@ -516,6 +510,11 @@ function createURLEncodedParser(options = {}) {
516510 body [ key ] = value
517511 }
518512 }
513+ } else {
514+ // Simple mode: flat key-value pairs, last value wins
515+ if ( ! PROTOTYPE_POLLUTION_KEYS . has ( key ) ) {
516+ body [ key ] = value
517+ }
519518 }
520519 }
521520
@@ -570,18 +569,6 @@ function createMultipartParser(options = {}) {
570569 const body = Object . create ( null )
571570 const files = Object . create ( null )
572571
573- // Prototype pollution blocklist for field names
574- const prototypePollutionKeys = [
575- '__proto__' ,
576- 'constructor' ,
577- 'prototype' ,
578- 'hasOwnProperty' ,
579- 'isPrototypeOf' ,
580- 'propertyIsEnumerable' ,
581- 'valueOf' ,
582- 'toString' ,
583- ]
584-
585572 for ( const [ key , value ] of formData . entries ( ) ) {
586573 fieldCount ++
587574 if ( fieldCount > maxFields ) {
@@ -594,7 +581,7 @@ function createMultipartParser(options = {}) {
594581 }
595582
596583 // Skip prototype pollution keys
597- if ( prototypePollutionKeys . includes ( key ) ) {
584+ if ( PROTOTYPE_POLLUTION_KEYS . has ( key ) ) {
598585 continue
599586 }
600587
@@ -710,6 +697,7 @@ function createMultipartParser(options = {}) {
710697 * @param {Function } options.onError - Custom error handler
711698 * @param {Function } options.verify - Body verification function
712699 * @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)
713701 * @param {string|number } options.jsonLimit - JSON size limit (backward compatibility)
714702 * @param {string|number } options.textLimit - Text size limit (backward compatibility)
715703 * @param {string|number } options.urlencodedLimit - URL-encoded size limit (backward compatibility)
@@ -727,6 +715,7 @@ function createBodyParser(options = {}) {
727715 onError,
728716 verify,
729717 parseNestedObjects = true ,
718+ extended,
730719 // Backward compatibility for direct limit options
731720 jsonLimit,
732721 textLimit,
@@ -750,6 +739,12 @@ function createBodyParser(options = {}) {
750739 urlencoded . urlencodedLimit ||
751740 urlencoded . limit ||
752741 '1mb' ,
742+ extended :
743+ urlencoded . extended !== undefined
744+ ? urlencoded . extended
745+ : extended !== undefined
746+ ? extended
747+ : true ,
753748 parseNestedObjects :
754749 urlencoded . parseNestedObjects !== undefined
755750 ? urlencoded . parseNestedObjects
0 commit comments