fastify 3.29.3 → 3.29.4
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/fastify.js +1 -1
- package/lib/contentTypeParser.js +79 -6
- package/package.json +2 -1
- package/test/content-parser.test.js +214 -0
- package/test/custom-parser.test.js +3 -3
package/fastify.js
CHANGED
package/lib/contentTypeParser.js
CHANGED
|
@@ -6,6 +6,7 @@ let lru = require('tiny-lru')
|
|
|
6
6
|
// See https://github.com/fastify/fastify/issues/2356
|
|
7
7
|
// and https://github.com/fastify/fastify/discussions/2907.
|
|
8
8
|
lru = typeof lru === 'function' ? lru : lru.default
|
|
9
|
+
const { parse: parseContentType } = require('content-type')
|
|
9
10
|
|
|
10
11
|
const secureJson = require('secure-json-parse')
|
|
11
12
|
const {
|
|
@@ -36,7 +37,7 @@ function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning)
|
|
|
36
37
|
this.customParsers = new Map()
|
|
37
38
|
this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
|
|
38
39
|
this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
|
|
39
|
-
this.parserList = ['application/json', 'text/plain']
|
|
40
|
+
this.parserList = [new ParserListItem('application/json'), new ParserListItem('text/plain')]
|
|
40
41
|
this.parserRegExpList = []
|
|
41
42
|
this.cache = lru(100)
|
|
42
43
|
}
|
|
@@ -69,7 +70,7 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
|
|
|
69
70
|
this.customParsers.set('', parser)
|
|
70
71
|
} else {
|
|
71
72
|
if (contentTypeIsString) {
|
|
72
|
-
this.parserList.unshift(contentType)
|
|
73
|
+
this.parserList.unshift(new ParserListItem(contentType))
|
|
73
74
|
} else {
|
|
74
75
|
this.parserRegExpList.unshift(contentType)
|
|
75
76
|
}
|
|
@@ -97,11 +98,25 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
97
98
|
return this.customParsers.get(contentType)
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
const parser = this.cache.get(contentType)
|
|
102
|
+
// TODO not covered by tests, this is a security backport
|
|
103
|
+
/* istanbul ignore next */
|
|
104
|
+
if (parser !== undefined) return parser
|
|
105
|
+
|
|
106
|
+
const parsed = safeParseContentType(contentType)
|
|
107
|
+
|
|
108
|
+
// dummyContentType always the same object
|
|
109
|
+
// we can use === for the comparsion and return early
|
|
110
|
+
if (parsed === dummyContentType) {
|
|
111
|
+
return this.customParsers.get('')
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
// eslint-disable-next-line no-var
|
|
101
115
|
for (var i = 0; i !== this.parserList.length; ++i) {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
const parser = this.customParsers.get(
|
|
116
|
+
const parserListItem = this.parserList[i]
|
|
117
|
+
if (compareContentType(parsed, parserListItem)) {
|
|
118
|
+
const parser = this.customParsers.get(parserListItem.name)
|
|
119
|
+
// we set request content-type in cache to reduce parsing of MIME type
|
|
105
120
|
this.cache.set(contentType, parser)
|
|
106
121
|
return parser
|
|
107
122
|
}
|
|
@@ -110,8 +125,9 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
110
125
|
// eslint-disable-next-line no-var
|
|
111
126
|
for (var j = 0; j !== this.parserRegExpList.length; ++j) {
|
|
112
127
|
const parserRegExp = this.parserRegExpList[j]
|
|
113
|
-
if (
|
|
128
|
+
if (compareRegExpContentType(contentType, parsed.type, parserRegExp)) {
|
|
114
129
|
const parser = this.customParsers.get(parserRegExp.toString())
|
|
130
|
+
// we set request content-type in cache to reduce parsing of MIME type
|
|
115
131
|
this.cache.set(contentType, parser)
|
|
116
132
|
return parser
|
|
117
133
|
}
|
|
@@ -348,6 +364,63 @@ function removeAllContentTypeParsers () {
|
|
|
348
364
|
this[kContentTypeParser].removeAll()
|
|
349
365
|
}
|
|
350
366
|
|
|
367
|
+
// dummy here to prevent repeated object creation
|
|
368
|
+
const dummyContentType = { type: '', parameters: Object.create(null) }
|
|
369
|
+
|
|
370
|
+
function safeParseContentType (contentType) {
|
|
371
|
+
try {
|
|
372
|
+
return parseContentType(contentType)
|
|
373
|
+
} catch (err) {
|
|
374
|
+
return dummyContentType
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function compareContentType (contentType, parserListItem) {
|
|
379
|
+
if (parserListItem.isEssence) {
|
|
380
|
+
// we do essence check
|
|
381
|
+
return contentType.type.indexOf(parserListItem) !== -1
|
|
382
|
+
} else {
|
|
383
|
+
// when the content-type includes parameters
|
|
384
|
+
// we do a full-text search
|
|
385
|
+
// reject essence content-type before checking parameters
|
|
386
|
+
if (contentType.type.indexOf(parserListItem.type) === -1) return false
|
|
387
|
+
for (const key of parserListItem.parameterKeys) {
|
|
388
|
+
// reject when missing parameters
|
|
389
|
+
if (!(key in contentType.parameters)) return false
|
|
390
|
+
// reject when parameters do not match
|
|
391
|
+
if (contentType.parameters[key] !== parserListItem.parameters[key]) return false
|
|
392
|
+
}
|
|
393
|
+
return true
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function compareRegExpContentType (contentType, essenceMIMEType, regexp) {
|
|
398
|
+
if (regexp.source.indexOf(';') === -1) {
|
|
399
|
+
// we do essence check
|
|
400
|
+
return regexp.test(essenceMIMEType)
|
|
401
|
+
} else {
|
|
402
|
+
// when the content-type includes parameters
|
|
403
|
+
// we do a full-text match
|
|
404
|
+
return regexp.test(contentType)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function ParserListItem (contentType) {
|
|
409
|
+
this.name = contentType
|
|
410
|
+
// we pre-calculate all the needed information
|
|
411
|
+
// before content-type comparsion
|
|
412
|
+
const parsed = safeParseContentType(contentType)
|
|
413
|
+
this.type = parsed.type
|
|
414
|
+
this.parameters = parsed.parameters
|
|
415
|
+
this.parameterKeys = Object.keys(parsed.parameters)
|
|
416
|
+
this.isEssence = contentType.indexOf(';') === -1
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// used in ContentTypeParser.remove
|
|
420
|
+
ParserListItem.prototype.toString = function () {
|
|
421
|
+
return this.name
|
|
422
|
+
}
|
|
423
|
+
|
|
351
424
|
module.exports = ContentTypeParser
|
|
352
425
|
module.exports.helpers = {
|
|
353
426
|
buildContentTypeParser,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
"version": "3.29.
|
|
3
|
+
"version": "3.29.4",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
"light-my-request": "^4.2.0",
|
|
188
188
|
"pino": "^6.13.0",
|
|
189
189
|
"process-warning": "^1.0.0",
|
|
190
|
+
"content-type": "^1.0.4",
|
|
190
191
|
"proxy-addr": "^2.0.7",
|
|
191
192
|
"rfdc": "^1.1.4",
|
|
192
193
|
"secure-json-parse": "^2.0.0",
|
|
@@ -328,3 +328,217 @@ test('Safeguard against malicious content-type / 3', async t => {
|
|
|
328
328
|
|
|
329
329
|
t.same(response.statusCode, 415)
|
|
330
330
|
})
|
|
331
|
+
|
|
332
|
+
test('Safeguard against content-type spoofing - string', async t => {
|
|
333
|
+
t.plan(1)
|
|
334
|
+
|
|
335
|
+
const fastify = Fastify()
|
|
336
|
+
fastify.removeAllContentTypeParsers()
|
|
337
|
+
fastify.addContentTypeParser('text/plain', function (request, body, done) {
|
|
338
|
+
t.pass('should be called')
|
|
339
|
+
done(null, body)
|
|
340
|
+
})
|
|
341
|
+
fastify.addContentTypeParser('application/json', function (request, body, done) {
|
|
342
|
+
t.fail('shouldn\'t be called')
|
|
343
|
+
done(null, body)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
fastify.post('/', async () => {
|
|
347
|
+
return 'ok'
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
await fastify.inject({
|
|
351
|
+
method: 'POST',
|
|
352
|
+
path: '/',
|
|
353
|
+
headers: {
|
|
354
|
+
'content-type': 'text/plain; content-type="application/json"'
|
|
355
|
+
},
|
|
356
|
+
body: ''
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test('Safeguard against content-type spoofing - regexp', async t => {
|
|
361
|
+
t.plan(1)
|
|
362
|
+
|
|
363
|
+
const fastify = Fastify()
|
|
364
|
+
fastify.removeAllContentTypeParsers()
|
|
365
|
+
fastify.addContentTypeParser(/text\/plain/, function (request, body, done) {
|
|
366
|
+
t.pass('should be called')
|
|
367
|
+
done(null, body)
|
|
368
|
+
})
|
|
369
|
+
fastify.addContentTypeParser(/application\/json/, function (request, body, done) {
|
|
370
|
+
t.fail('shouldn\'t be called')
|
|
371
|
+
done(null, body)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
fastify.post('/', async () => {
|
|
375
|
+
return 'ok'
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
await fastify.inject({
|
|
379
|
+
method: 'POST',
|
|
380
|
+
path: '/',
|
|
381
|
+
headers: {
|
|
382
|
+
'content-type': 'text/plain; content-type="application/json"'
|
|
383
|
+
},
|
|
384
|
+
body: ''
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('content-type match parameters - string 1', async t => {
|
|
389
|
+
t.plan(1)
|
|
390
|
+
|
|
391
|
+
const fastify = Fastify()
|
|
392
|
+
fastify.removeAllContentTypeParsers()
|
|
393
|
+
fastify.addContentTypeParser('text/plain; charset=utf8', function (request, body, done) {
|
|
394
|
+
t.fail('shouldn\'t be called')
|
|
395
|
+
done(null, body)
|
|
396
|
+
})
|
|
397
|
+
fastify.addContentTypeParser('application/json; charset=utf8', function (request, body, done) {
|
|
398
|
+
t.pass('should be called')
|
|
399
|
+
done(null, body)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
fastify.post('/', async () => {
|
|
403
|
+
return 'ok'
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
await fastify.inject({
|
|
407
|
+
method: 'POST',
|
|
408
|
+
path: '/',
|
|
409
|
+
headers: {
|
|
410
|
+
'content-type': 'application/json; charset=utf8'
|
|
411
|
+
},
|
|
412
|
+
body: ''
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('content-type match parameters - string 2', async t => {
|
|
417
|
+
t.plan(1)
|
|
418
|
+
|
|
419
|
+
const fastify = Fastify()
|
|
420
|
+
fastify.removeAllContentTypeParsers()
|
|
421
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
422
|
+
t.pass('should be called')
|
|
423
|
+
done(null, body)
|
|
424
|
+
})
|
|
425
|
+
fastify.addContentTypeParser('text/plain; charset=utf8; foo=bar', function (request, body, done) {
|
|
426
|
+
t.fail('shouldn\'t be called')
|
|
427
|
+
done(null, body)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
fastify.post('/', async () => {
|
|
431
|
+
return 'ok'
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
await fastify.inject({
|
|
435
|
+
method: 'POST',
|
|
436
|
+
path: '/',
|
|
437
|
+
headers: {
|
|
438
|
+
'content-type': 'application/json; foo=bar; charset=utf8'
|
|
439
|
+
},
|
|
440
|
+
body: ''
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('content-type match parameters - regexp', async t => {
|
|
445
|
+
t.plan(1)
|
|
446
|
+
|
|
447
|
+
const fastify = Fastify()
|
|
448
|
+
fastify.removeAllContentTypeParsers()
|
|
449
|
+
fastify.addContentTypeParser(/application\/json; charset=utf8/, function (request, body, done) {
|
|
450
|
+
t.pass('should be called')
|
|
451
|
+
done(null, body)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
fastify.post('/', async () => {
|
|
455
|
+
return 'ok'
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
await fastify.inject({
|
|
459
|
+
method: 'POST',
|
|
460
|
+
path: '/',
|
|
461
|
+
headers: {
|
|
462
|
+
'content-type': 'application/json; charset=utf8'
|
|
463
|
+
},
|
|
464
|
+
body: ''
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('content-type fail when parameters not match - string 1', async t => {
|
|
469
|
+
t.plan(1)
|
|
470
|
+
|
|
471
|
+
const fastify = Fastify()
|
|
472
|
+
fastify.removeAllContentTypeParsers()
|
|
473
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
474
|
+
t.fail('shouldn\'t be called')
|
|
475
|
+
done(null, body)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
fastify.post('/', async () => {
|
|
479
|
+
return 'ok'
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
const response = await fastify.inject({
|
|
483
|
+
method: 'POST',
|
|
484
|
+
path: '/',
|
|
485
|
+
headers: {
|
|
486
|
+
'content-type': 'application/json; charset=utf8'
|
|
487
|
+
},
|
|
488
|
+
body: ''
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
t.same(response.statusCode, 415)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
test('content-type fail when parameters not match - string 2', async t => {
|
|
495
|
+
t.plan(1)
|
|
496
|
+
|
|
497
|
+
const fastify = Fastify()
|
|
498
|
+
fastify.removeAllContentTypeParsers()
|
|
499
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
500
|
+
t.fail('shouldn\'t be called')
|
|
501
|
+
done(null, body)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
fastify.post('/', async () => {
|
|
505
|
+
return 'ok'
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const response = await fastify.inject({
|
|
509
|
+
method: 'POST',
|
|
510
|
+
path: '/',
|
|
511
|
+
headers: {
|
|
512
|
+
'content-type': 'application/json; charset=utf8; foo=baz'
|
|
513
|
+
},
|
|
514
|
+
body: ''
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
t.same(response.statusCode, 415)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
test('content-type fail when parameters not match - regexp', async t => {
|
|
521
|
+
t.plan(1)
|
|
522
|
+
|
|
523
|
+
const fastify = Fastify()
|
|
524
|
+
fastify.removeAllContentTypeParsers()
|
|
525
|
+
fastify.addContentTypeParser(/application\/json; charset=utf8; foo=bar/, function (request, body, done) {
|
|
526
|
+
t.fail('shouldn\'t be called')
|
|
527
|
+
done(null, body)
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
fastify.post('/', async () => {
|
|
531
|
+
return 'ok'
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const response = await fastify.inject({
|
|
535
|
+
method: 'POST',
|
|
536
|
+
path: '/',
|
|
537
|
+
headers: {
|
|
538
|
+
'content-type': 'application/json; charset=utf8'
|
|
539
|
+
},
|
|
540
|
+
body: ''
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
t.same(response.statusCode, 415)
|
|
544
|
+
})
|
|
@@ -1120,7 +1120,7 @@ test('The charset should not interfere with the content type handling', t => {
|
|
|
1120
1120
|
url: 'http://localhost:' + fastify.server.address().port,
|
|
1121
1121
|
body: '{"hello":"world"}',
|
|
1122
1122
|
headers: {
|
|
1123
|
-
'Content-Type': 'application/json charset=utf-8'
|
|
1123
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
1124
1124
|
}
|
|
1125
1125
|
}, (err, response, body) => {
|
|
1126
1126
|
t.error(err)
|
|
@@ -1303,7 +1303,7 @@ test('contentTypeParser should add a custom parser with RegExp value', t => {
|
|
|
1303
1303
|
url: 'http://localhost:' + fastify.server.address().port,
|
|
1304
1304
|
body: '{"hello":"world"}',
|
|
1305
1305
|
headers: {
|
|
1306
|
-
'Content-Type': 'weird
|
|
1306
|
+
'Content-Type': 'weird/content-type+json'
|
|
1307
1307
|
}
|
|
1308
1308
|
}, (err, response, body) => {
|
|
1309
1309
|
t.error(err)
|
|
@@ -1333,7 +1333,7 @@ test('contentTypeParser should add multiple custom parsers with RegExp values',
|
|
|
1333
1333
|
done(null, 'xml')
|
|
1334
1334
|
})
|
|
1335
1335
|
|
|
1336
|
-
fastify.addContentTypeParser(/.*\+myExtension
|
|
1336
|
+
fastify.addContentTypeParser(/.*\+myExtension$/i, function (req, payload, done) {
|
|
1337
1337
|
let data = ''
|
|
1338
1338
|
payload.on('data', chunk => { data += chunk })
|
|
1339
1339
|
payload.on('end', () => {
|