0http-bun 1.1.3 → 1.2.1
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.
- package/README.md +72 -3
- package/common.d.ts +37 -2
- package/lib/middleware/README.md +870 -0
- package/lib/middleware/body-parser.js +783 -0
- package/lib/middleware/cors.js +225 -0
- package/lib/middleware/index.d.ts +236 -0
- package/lib/middleware/index.js +45 -0
- package/lib/middleware/jwt-auth.js +406 -0
- package/lib/middleware/logger.js +310 -0
- package/lib/middleware/rate-limit.js +270 -0
- package/lib/router/sequential.js +10 -2
- package/package.json +6 -3
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Body Parser Middleware for 0http-bun
|
|
3
|
+
* Supports JSON, text, URL-encoded, and multipart form data parsing
|
|
4
|
+
*
|
|
5
|
+
* Security Features:
|
|
6
|
+
* - Protected against prototype pollution attacks
|
|
7
|
+
* - ReDoS (Regular Expression Denial of Service) protection
|
|
8
|
+
* - Memory exhaustion prevention with strict size limits
|
|
9
|
+
* - Excessive nesting protection for JSON
|
|
10
|
+
* - Parameter count limits for form data
|
|
11
|
+
* - Input validation and sanitization
|
|
12
|
+
* - Error message sanitization to prevent information leakage
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses size limit strings with suffixes (e.g. '500b', '1kb', '2mb')
|
|
17
|
+
* @param {number|string} limit - Size limit
|
|
18
|
+
* @returns {number} Size in bytes
|
|
19
|
+
*/
|
|
20
|
+
function parseLimit(limit) {
|
|
21
|
+
if (typeof limit === 'number') {
|
|
22
|
+
// Enforce maximum limit to prevent memory exhaustion
|
|
23
|
+
return Math.min(Math.max(0, limit), 1024 * 1024 * 1024) // Max 1GB
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof limit === 'string') {
|
|
27
|
+
// Prevent ReDoS by limiting string length and using a more restrictive regex
|
|
28
|
+
if (limit.length > 20) {
|
|
29
|
+
throw new Error(`Invalid limit format: ${limit}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// More restrictive regex to prevent ReDoS attacks
|
|
33
|
+
const match = limit.match(/^(\d{1,10}(?:\.\d{1,3})?)\s*(b|kb|mb|gb)$/i)
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw new Error(`Invalid limit format: ${limit}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const value = parseFloat(match[1])
|
|
39
|
+
if (isNaN(value) || value < 0) {
|
|
40
|
+
throw new Error(`Invalid limit value: ${limit}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const unit = match[2].toLowerCase()
|
|
44
|
+
|
|
45
|
+
let bytes
|
|
46
|
+
switch (unit) {
|
|
47
|
+
case 'b':
|
|
48
|
+
bytes = value
|
|
49
|
+
break
|
|
50
|
+
case 'kb':
|
|
51
|
+
bytes = value * 1024
|
|
52
|
+
break
|
|
53
|
+
case 'mb':
|
|
54
|
+
bytes = value * 1024 * 1024
|
|
55
|
+
break
|
|
56
|
+
case 'gb':
|
|
57
|
+
bytes = value * 1024 * 1024 * 1024
|
|
58
|
+
break
|
|
59
|
+
default:
|
|
60
|
+
bytes = value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Enforce maximum limit to prevent memory exhaustion
|
|
64
|
+
return Math.min(bytes, 1024 * 1024 * 1024) // Max 1GB
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return limit || 1024 * 1024 // Default 1MB
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Helper function to check if content type matches any of the JSON types
|
|
72
|
+
* @param {string} contentType - Content type from request
|
|
73
|
+
* @param {string[]} jsonTypes - Array of JSON content types
|
|
74
|
+
* @returns {boolean} Whether content type matches any JSON type
|
|
75
|
+
*/
|
|
76
|
+
function isJsonType(contentType, jsonTypes) {
|
|
77
|
+
if (!contentType) return false
|
|
78
|
+
const lowerContentType = contentType.toLowerCase()
|
|
79
|
+
return jsonTypes.some((type) => lowerContentType.includes(type.toLowerCase()))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Helper function to check if request method typically has a body
|
|
84
|
+
* @param {Request} req - Request object
|
|
85
|
+
* @returns {boolean} Whether the request method has a body
|
|
86
|
+
*/
|
|
87
|
+
function hasBody(req) {
|
|
88
|
+
return ['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Helper function to check if content type should be parsed
|
|
93
|
+
* @param {Request} req - Request object
|
|
94
|
+
* @param {string} type - Expected content type
|
|
95
|
+
* @returns {boolean} Whether content should be parsed
|
|
96
|
+
*/
|
|
97
|
+
function shouldParse(req, type) {
|
|
98
|
+
const contentType = req.headers.get('content-type')
|
|
99
|
+
return contentType && contentType.toLowerCase().includes(type.toLowerCase())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Helper function to parse nested keys in URL-encoded data
|
|
104
|
+
* Protected against prototype pollution attacks
|
|
105
|
+
* @param {Object} obj - Target object
|
|
106
|
+
* @param {string} key - Key with potential nesting
|
|
107
|
+
* @param {string} value - Value to set
|
|
108
|
+
* @param {number} depth - Current nesting depth to prevent excessive recursion
|
|
109
|
+
*/
|
|
110
|
+
function parseNestedKey(obj, key, value, depth = 0) {
|
|
111
|
+
// Prevent excessive nesting to avoid stack overflow
|
|
112
|
+
if (depth > 20) {
|
|
113
|
+
throw new Error('Maximum nesting depth exceeded')
|
|
114
|
+
}
|
|
115
|
+
|
|
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)) {
|
|
129
|
+
return // Silently ignore dangerous keys
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const match = key.match(/^([^[]+)\[([^\]]*)\](.*)$/)
|
|
133
|
+
if (!match) {
|
|
134
|
+
obj[key] = value
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const [, baseKey, indexKey, remaining] = match
|
|
139
|
+
|
|
140
|
+
// Protect against prototype pollution on base key
|
|
141
|
+
if (prototypePollutionKeys.includes(baseKey)) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!obj[baseKey]) {
|
|
146
|
+
obj[baseKey] = indexKey === '' ? [] : {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Ensure obj[baseKey] is a safe object/array
|
|
150
|
+
if (typeof obj[baseKey] !== 'object' || obj[baseKey] === null) {
|
|
151
|
+
obj[baseKey] = indexKey === '' ? [] : {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (remaining) {
|
|
155
|
+
const nextKey = indexKey + remaining
|
|
156
|
+
parseNestedKey(obj[baseKey], nextKey, value, depth + 1)
|
|
157
|
+
} else {
|
|
158
|
+
if (indexKey === '') {
|
|
159
|
+
if (Array.isArray(obj[baseKey])) {
|
|
160
|
+
obj[baseKey].push(value)
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// Protect against prototype pollution on index key
|
|
164
|
+
if (!prototypePollutionKeys.includes(indexKey)) {
|
|
165
|
+
obj[baseKey][indexKey] = value
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a JSON body parser middleware
|
|
173
|
+
* @param {Object} options - Body parser configuration
|
|
174
|
+
* @param {number|string} options.limit - Maximum body size in bytes (default: 1MB)
|
|
175
|
+
* @param {Function} options.reviver - JSON.parse reviver function
|
|
176
|
+
* @param {boolean} options.strict - Only parse arrays and objects (default: true)
|
|
177
|
+
* @param {string} options.type - Content-Type to parse (default: application/json)
|
|
178
|
+
* @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
|
|
179
|
+
* @returns {Function} Middleware function
|
|
180
|
+
*/
|
|
181
|
+
function createJSONParser(options = {}) {
|
|
182
|
+
const {
|
|
183
|
+
limit = '1mb',
|
|
184
|
+
reviver,
|
|
185
|
+
strict = true,
|
|
186
|
+
type = 'application/json',
|
|
187
|
+
deferNext = false,
|
|
188
|
+
} = options
|
|
189
|
+
|
|
190
|
+
const parsedLimit = parseLimit(limit)
|
|
191
|
+
|
|
192
|
+
return async function jsonParserMiddleware(req, next) {
|
|
193
|
+
if (!hasBody(req) || !shouldParse(req, type)) {
|
|
194
|
+
return deferNext ? null : next()
|
|
195
|
+
}
|
|
196
|
+
|
|
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
|
+
}
|
|
207
|
+
}
|
|
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()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const text = await req.text()
|
|
221
|
+
// Store raw body text for verification
|
|
222
|
+
req._rawBodyText = text
|
|
223
|
+
|
|
224
|
+
// Validate text length to prevent memory exhaustion
|
|
225
|
+
const textLength = new TextEncoder().encode(text).length
|
|
226
|
+
if (textLength > parsedLimit) {
|
|
227
|
+
return new Response('Request body size exceeded', {status: 413})
|
|
228
|
+
}
|
|
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
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (maxNesting > 100) {
|
|
244
|
+
return new Response('JSON nesting too deep', {status: 400})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
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')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Object.defineProperty(req, 'body', {
|
|
271
|
+
value: body,
|
|
272
|
+
writable: true,
|
|
273
|
+
enumerable: true,
|
|
274
|
+
configurable: true,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
return deferNext ? null : next()
|
|
278
|
+
} catch (error) {
|
|
279
|
+
throw error
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Creates a text body parser middleware
|
|
286
|
+
* @param {Object} options - Body parser configuration
|
|
287
|
+
* @param {number|string} options.limit - Maximum body size in bytes
|
|
288
|
+
* @param {string} options.type - Content-Type to parse (default: text/*)
|
|
289
|
+
* @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
|
|
290
|
+
* @returns {Function} Middleware function
|
|
291
|
+
*/
|
|
292
|
+
function createTextParser(options = {}) {
|
|
293
|
+
const {limit = '1mb', type = 'text/', deferNext = false} = options
|
|
294
|
+
|
|
295
|
+
const parsedLimit = parseLimit(limit)
|
|
296
|
+
|
|
297
|
+
return async function textParserMiddleware(req, next) {
|
|
298
|
+
if (!hasBody(req) || !shouldParse(req, type)) {
|
|
299
|
+
return deferNext ? null : next()
|
|
300
|
+
}
|
|
301
|
+
|
|
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
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
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) {
|
|
320
|
+
return new Response('Request body size exceeded', {status: 413})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
Object.defineProperty(req, 'body', {
|
|
324
|
+
value: text,
|
|
325
|
+
writable: true,
|
|
326
|
+
enumerable: true,
|
|
327
|
+
configurable: true,
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
return deferNext ? null : next()
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw error
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Creates a URL-encoded form parser middleware
|
|
339
|
+
* @param {Object} options - Body parser configuration
|
|
340
|
+
* @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
|
|
343
|
+
* @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
|
|
344
|
+
* @returns {Function} Middleware function
|
|
345
|
+
*/
|
|
346
|
+
function createURLEncodedParser(options = {}) {
|
|
347
|
+
const {
|
|
348
|
+
limit = '1mb',
|
|
349
|
+
extended = true,
|
|
350
|
+
parseNestedObjects = true,
|
|
351
|
+
deferNext = false,
|
|
352
|
+
} = options
|
|
353
|
+
|
|
354
|
+
const parsedLimit = parseLimit(limit)
|
|
355
|
+
|
|
356
|
+
return async function urlEncodedParserMiddleware(req, next) {
|
|
357
|
+
if (
|
|
358
|
+
!hasBody(req) ||
|
|
359
|
+
!shouldParse(req, 'application/x-www-form-urlencoded')
|
|
360
|
+
) {
|
|
361
|
+
return deferNext ? null : next()
|
|
362
|
+
}
|
|
363
|
+
|
|
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
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
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) {
|
|
382
|
+
return new Response('Request body size exceeded', {status: 413})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const body = {}
|
|
386
|
+
const params = new URLSearchParams(text)
|
|
387
|
+
|
|
388
|
+
// Prevent DoS through excessive parameters
|
|
389
|
+
let paramCount = 0
|
|
390
|
+
const maxParams = 1000 // Reasonable limit for URL-encoded parameters
|
|
391
|
+
|
|
392
|
+
for (const [key, value] of params.entries()) {
|
|
393
|
+
paramCount++
|
|
394
|
+
if (paramCount > maxParams) {
|
|
395
|
+
return new Response('Too many parameters', {status: 400})
|
|
396
|
+
}
|
|
397
|
+
|
|
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
|
+
}
|
|
402
|
+
|
|
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
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
body[key] = value
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
Object.defineProperty(req, 'body', {
|
|
440
|
+
value: body,
|
|
441
|
+
writable: true,
|
|
442
|
+
enumerable: true,
|
|
443
|
+
configurable: true,
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
return deferNext ? null : next()
|
|
447
|
+
} catch (error) {
|
|
448
|
+
throw error
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Creates a multipart/form-data parser middleware
|
|
455
|
+
* @param {Object} options - Body parser configuration
|
|
456
|
+
* @param {number|string} options.limit - Maximum body size in bytes
|
|
457
|
+
* @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
|
|
458
|
+
* @returns {Function} Middleware function
|
|
459
|
+
*/
|
|
460
|
+
function createMultipartParser(options = {}) {
|
|
461
|
+
const {limit = '10mb', deferNext = false} = options
|
|
462
|
+
|
|
463
|
+
const parsedLimit = parseLimit(limit)
|
|
464
|
+
|
|
465
|
+
return async function multipartParserMiddleware(req, next) {
|
|
466
|
+
const contentType = req.headers.get('content-type')
|
|
467
|
+
if (!hasBody(req) || !contentType?.startsWith('multipart/form-data')) {
|
|
468
|
+
return deferNext ? null : next()
|
|
469
|
+
}
|
|
470
|
+
|
|
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
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const formData = await req.formData()
|
|
484
|
+
|
|
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
|
|
489
|
+
|
|
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
|
+
}
|
|
495
|
+
|
|
496
|
+
// Validate field name length
|
|
497
|
+
if (key.length > 1000) {
|
|
498
|
+
return new Response('Field name too long', {status: 400})
|
|
499
|
+
}
|
|
500
|
+
|
|
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
|
+
}
|
|
519
|
+
}
|
|
520
|
+
totalSize += new TextEncoder().encode(key).length
|
|
521
|
+
|
|
522
|
+
// Check total size periodically to prevent memory exhaustion
|
|
523
|
+
if (totalSize > parsedLimit) {
|
|
524
|
+
return new Response('Request body size exceeded', {status: 413})
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
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())
|
|
535
|
+
files[key] = {
|
|
536
|
+
filename: value.name,
|
|
537
|
+
name: value.name,
|
|
538
|
+
size: value.size,
|
|
539
|
+
type: value.type,
|
|
540
|
+
mimetype: mimetype,
|
|
541
|
+
data: fileData,
|
|
542
|
+
}
|
|
543
|
+
} 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
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
Object.defineProperty(req, 'body', {
|
|
557
|
+
value: body,
|
|
558
|
+
writable: true,
|
|
559
|
+
enumerable: true,
|
|
560
|
+
configurable: true,
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
Object.defineProperty(req, 'files', {
|
|
564
|
+
value: files,
|
|
565
|
+
writable: true,
|
|
566
|
+
enumerable: true,
|
|
567
|
+
configurable: true,
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
return deferNext ? null : next()
|
|
571
|
+
} catch (error) {
|
|
572
|
+
throw error
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Combines multiple body parsers based on content type
|
|
579
|
+
* @param {Object} options - Configuration for each parser type
|
|
580
|
+
* @param {Object} options.json - JSON parser options
|
|
581
|
+
* @param {Object} options.text - Text parser options
|
|
582
|
+
* @param {Object} options.urlencoded - URL-encoded parser options
|
|
583
|
+
* @param {Object} options.multipart - Multipart parser options
|
|
584
|
+
* @param {string[]} options.jsonTypes - Custom JSON content types
|
|
585
|
+
* @param {Function} options.jsonParser - Custom JSON parser function
|
|
586
|
+
* @param {Function} options.onError - Custom error handler
|
|
587
|
+
* @param {Function} options.verify - Body verification function
|
|
588
|
+
* @param {boolean} options.parseNestedObjects - Parse nested object notation (for compatibility)
|
|
589
|
+
* @param {string|number} options.jsonLimit - JSON size limit (backward compatibility)
|
|
590
|
+
* @param {string|number} options.textLimit - Text size limit (backward compatibility)
|
|
591
|
+
* @param {string|number} options.urlencodedLimit - URL-encoded size limit (backward compatibility)
|
|
592
|
+
* @param {string|number} options.multipartLimit - Multipart size limit (backward compatibility)
|
|
593
|
+
* @returns {Function} Middleware function
|
|
594
|
+
*/
|
|
595
|
+
function createBodyParser(options = {}) {
|
|
596
|
+
const {
|
|
597
|
+
json = {},
|
|
598
|
+
text = {},
|
|
599
|
+
urlencoded = {},
|
|
600
|
+
multipart = {},
|
|
601
|
+
jsonTypes = ['application/json'],
|
|
602
|
+
jsonParser,
|
|
603
|
+
onError,
|
|
604
|
+
verify,
|
|
605
|
+
parseNestedObjects = true,
|
|
606
|
+
// Backward compatibility for direct limit options
|
|
607
|
+
jsonLimit,
|
|
608
|
+
textLimit,
|
|
609
|
+
urlencodedLimit,
|
|
610
|
+
multipartLimit,
|
|
611
|
+
} = options
|
|
612
|
+
|
|
613
|
+
// Map configuration keys to actual limits for the parsers
|
|
614
|
+
const jsonOptions = {
|
|
615
|
+
...json,
|
|
616
|
+
limit: jsonLimit || json.jsonLimit || json.limit || '1mb',
|
|
617
|
+
}
|
|
618
|
+
const textOptions = {
|
|
619
|
+
...text,
|
|
620
|
+
limit: textLimit || text.textLimit || text.limit || '1mb',
|
|
621
|
+
}
|
|
622
|
+
const urlencodedOptions = {
|
|
623
|
+
...urlencoded,
|
|
624
|
+
limit:
|
|
625
|
+
urlencodedLimit ||
|
|
626
|
+
urlencoded.urlencodedLimit ||
|
|
627
|
+
urlencoded.limit ||
|
|
628
|
+
'1mb',
|
|
629
|
+
parseNestedObjects:
|
|
630
|
+
urlencoded.parseNestedObjects !== undefined
|
|
631
|
+
? urlencoded.parseNestedObjects
|
|
632
|
+
: parseNestedObjects,
|
|
633
|
+
}
|
|
634
|
+
const multipartOptions = {
|
|
635
|
+
...multipart,
|
|
636
|
+
limit:
|
|
637
|
+
multipartLimit || multipart.multipartLimit || multipart.limit || '10mb',
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Create parsers with custom types consideration
|
|
641
|
+
const jsonParserMiddleware = createJSONParser({
|
|
642
|
+
...jsonOptions,
|
|
643
|
+
type: 'application/', // Broad match for JSON types
|
|
644
|
+
deferNext: !!verify, // Defer next if verification is enabled
|
|
645
|
+
})
|
|
646
|
+
const textParserMiddleware = createTextParser({
|
|
647
|
+
...textOptions,
|
|
648
|
+
deferNext: !!verify,
|
|
649
|
+
})
|
|
650
|
+
const urlEncodedParserMiddleware = createURLEncodedParser({
|
|
651
|
+
...urlencodedOptions,
|
|
652
|
+
deferNext: !!verify,
|
|
653
|
+
})
|
|
654
|
+
const multipartParserMiddleware = createMultipartParser({
|
|
655
|
+
...multipartOptions,
|
|
656
|
+
deferNext: !!verify,
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
return async function bodyParserMiddleware(req, next) {
|
|
660
|
+
const contentType = req.headers.get('content-type')
|
|
661
|
+
|
|
662
|
+
// For GET requests or requests without body, set body to undefined
|
|
663
|
+
if (!hasBody(req)) {
|
|
664
|
+
Object.defineProperty(req, 'body', {
|
|
665
|
+
value: undefined,
|
|
666
|
+
writable: true,
|
|
667
|
+
enumerable: true,
|
|
668
|
+
configurable: true,
|
|
669
|
+
})
|
|
670
|
+
return next()
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// If no content type, set body to undefined
|
|
674
|
+
if (!contentType) {
|
|
675
|
+
Object.defineProperty(req, 'body', {
|
|
676
|
+
value: undefined,
|
|
677
|
+
writable: true,
|
|
678
|
+
enumerable: true,
|
|
679
|
+
configurable: true,
|
|
680
|
+
})
|
|
681
|
+
return next()
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
let result
|
|
686
|
+
|
|
687
|
+
// Custom JSON parser handling for custom JSON types (case-insensitive)
|
|
688
|
+
if (jsonParser && isJsonType(contentType, jsonTypes)) {
|
|
689
|
+
const text = await req.text()
|
|
690
|
+
const body = jsonParser(text)
|
|
691
|
+
Object.defineProperty(req, 'body', {
|
|
692
|
+
value: body,
|
|
693
|
+
writable: true,
|
|
694
|
+
enumerable: true,
|
|
695
|
+
configurable: true,
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
// No result set, will be handled after verification
|
|
699
|
+
} else {
|
|
700
|
+
// Check if content type matches any JSON types first (including custom ones)
|
|
701
|
+
if (isJsonType(contentType, jsonTypes)) {
|
|
702
|
+
result = await jsonParserMiddleware(req, next)
|
|
703
|
+
} else {
|
|
704
|
+
// Route to appropriate parser based on content type (case-insensitive)
|
|
705
|
+
const lowerContentType = contentType.toLowerCase()
|
|
706
|
+
if (lowerContentType.includes('application/json')) {
|
|
707
|
+
result = await jsonParserMiddleware(req, next)
|
|
708
|
+
} else if (
|
|
709
|
+
lowerContentType.includes('application/x-www-form-urlencoded')
|
|
710
|
+
) {
|
|
711
|
+
result = await urlEncodedParserMiddleware(req, next)
|
|
712
|
+
} else if (lowerContentType.includes('multipart/form-data')) {
|
|
713
|
+
result = await multipartParserMiddleware(req, next)
|
|
714
|
+
} else if (lowerContentType.includes('text/')) {
|
|
715
|
+
result = await textParserMiddleware(req, next)
|
|
716
|
+
} else {
|
|
717
|
+
// For unsupported content types, set body to undefined
|
|
718
|
+
Object.defineProperty(req, 'body', {
|
|
719
|
+
value: undefined,
|
|
720
|
+
writable: true,
|
|
721
|
+
enumerable: true,
|
|
722
|
+
configurable: true,
|
|
723
|
+
})
|
|
724
|
+
result = verify ? null : next() // Defer if verification enabled
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// If a parser returned an error response, return it immediately
|
|
730
|
+
if (result && result instanceof Response) {
|
|
731
|
+
return result
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Apply verification after parsing if provided
|
|
735
|
+
if (verify && req.body !== undefined) {
|
|
736
|
+
try {
|
|
737
|
+
// For verification, we need to pass the raw body text
|
|
738
|
+
// Get the original text/data that was parsed
|
|
739
|
+
let rawBody = ''
|
|
740
|
+
if (req._rawBodyText) {
|
|
741
|
+
rawBody = req._rawBodyText
|
|
742
|
+
}
|
|
743
|
+
verify(req, rawBody)
|
|
744
|
+
} catch (verifyError) {
|
|
745
|
+
// Sanitize error message to prevent information leakage
|
|
746
|
+
const sanitizedMessage = verifyError.message
|
|
747
|
+
? verifyError.message.substring(0, 100)
|
|
748
|
+
: 'Verification failed'
|
|
749
|
+
return new Response(`Verification failed: ${sanitizedMessage}`, {
|
|
750
|
+
status: 400,
|
|
751
|
+
})
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// If result is null (deferred) or verification passed, call next
|
|
756
|
+
return result || next()
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (onError) {
|
|
759
|
+
return onError(error, req, next)
|
|
760
|
+
}
|
|
761
|
+
// Sanitize error message to prevent information leakage
|
|
762
|
+
const sanitizedMessage = error.message
|
|
763
|
+
? error.message.substring(0, 100)
|
|
764
|
+
: 'Body parsing failed'
|
|
765
|
+
return new Response(sanitizedMessage, {status: 400})
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// CommonJS exports
|
|
771
|
+
module.exports = {
|
|
772
|
+
createBodyParser,
|
|
773
|
+
createJSONParser,
|
|
774
|
+
createTextParser,
|
|
775
|
+
createURLEncodedParser,
|
|
776
|
+
createMultipartParser,
|
|
777
|
+
hasBody,
|
|
778
|
+
shouldParse,
|
|
779
|
+
parseLimit,
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Default export is the main body parser
|
|
783
|
+
module.exports.default = createBodyParser
|