fastify 4.10.1 → 4.10.2
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 +75 -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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { AsyncResource } = require('async_hooks')
|
|
4
4
|
const lru = require('tiny-lru').lru
|
|
5
|
+
// TODO: find more perforamant solution
|
|
6
|
+
const { parse: parseContentType } = require('content-type')
|
|
5
7
|
|
|
6
8
|
const secureJson = require('secure-json-parse')
|
|
7
9
|
const {
|
|
@@ -33,7 +35,7 @@ function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning)
|
|
|
33
35
|
this.customParsers = new Map()
|
|
34
36
|
this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
|
|
35
37
|
this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
|
|
36
|
-
this.parserList = ['application/json', 'text/plain']
|
|
38
|
+
this.parserList = [new ParserListItem('application/json'), new ParserListItem('text/plain')]
|
|
37
39
|
this.parserRegExpList = []
|
|
38
40
|
this.cache = lru(100)
|
|
39
41
|
}
|
|
@@ -66,7 +68,7 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
|
|
|
66
68
|
this.customParsers.set('', parser)
|
|
67
69
|
} else {
|
|
68
70
|
if (contentTypeIsString) {
|
|
69
|
-
this.parserList.unshift(contentType)
|
|
71
|
+
this.parserList.unshift(new ParserListItem(contentType))
|
|
70
72
|
} else {
|
|
71
73
|
this.parserRegExpList.unshift(contentType)
|
|
72
74
|
}
|
|
@@ -97,11 +99,20 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
97
99
|
const parser = this.cache.get(contentType)
|
|
98
100
|
if (parser !== undefined) return parser
|
|
99
101
|
|
|
102
|
+
const parsed = safeParseContentType(contentType)
|
|
103
|
+
|
|
104
|
+
// dummyContentType always the same object
|
|
105
|
+
// we can use === for the comparsion and return early
|
|
106
|
+
if (parsed === dummyContentType) {
|
|
107
|
+
return this.customParsers.get('')
|
|
108
|
+
}
|
|
109
|
+
|
|
100
110
|
// eslint-disable-next-line no-var
|
|
101
111
|
for (var i = 0; i !== this.parserList.length; ++i) {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
const parser = this.customParsers.get(
|
|
112
|
+
const parserListItem = this.parserList[i]
|
|
113
|
+
if (compareContentType(parsed, parserListItem)) {
|
|
114
|
+
const parser = this.customParsers.get(parserListItem.name)
|
|
115
|
+
// we set request content-type in cache to reduce parsing of MIME type
|
|
105
116
|
this.cache.set(contentType, parser)
|
|
106
117
|
return parser
|
|
107
118
|
}
|
|
@@ -110,8 +121,9 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
110
121
|
// eslint-disable-next-line no-var
|
|
111
122
|
for (var j = 0; j !== this.parserRegExpList.length; ++j) {
|
|
112
123
|
const parserRegExp = this.parserRegExpList[j]
|
|
113
|
-
if (
|
|
124
|
+
if (compareRegExpContentType(contentType, parsed.type, parserRegExp)) {
|
|
114
125
|
const parser = this.customParsers.get(parserRegExp.toString())
|
|
126
|
+
// we set request content-type in cache to reduce parsing of MIME type
|
|
115
127
|
this.cache.set(contentType, parser)
|
|
116
128
|
return parser
|
|
117
129
|
}
|
|
@@ -346,6 +358,63 @@ function removeAllContentTypeParsers () {
|
|
|
346
358
|
this[kContentTypeParser].removeAll()
|
|
347
359
|
}
|
|
348
360
|
|
|
361
|
+
// dummy here to prevent repeated object creation
|
|
362
|
+
const dummyContentType = { type: '', parameters: Object.create(null) }
|
|
363
|
+
|
|
364
|
+
function safeParseContentType (contentType) {
|
|
365
|
+
try {
|
|
366
|
+
return parseContentType(contentType)
|
|
367
|
+
} catch (err) {
|
|
368
|
+
return dummyContentType
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function compareContentType (contentType, parserListItem) {
|
|
373
|
+
if (parserListItem.isEssence) {
|
|
374
|
+
// we do essence check
|
|
375
|
+
return contentType.type.indexOf(parserListItem) !== -1
|
|
376
|
+
} else {
|
|
377
|
+
// when the content-type includes parameters
|
|
378
|
+
// we do a full-text search
|
|
379
|
+
// reject essence content-type before checking parameters
|
|
380
|
+
if (contentType.type.indexOf(parserListItem.type) === -1) return false
|
|
381
|
+
for (const key of parserListItem.parameterKeys) {
|
|
382
|
+
// reject when missing parameters
|
|
383
|
+
if (!(key in contentType.parameters)) return false
|
|
384
|
+
// reject when parameters do not match
|
|
385
|
+
if (contentType.parameters[key] !== parserListItem.parameters[key]) return false
|
|
386
|
+
}
|
|
387
|
+
return true
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function compareRegExpContentType (contentType, essenceMIMEType, regexp) {
|
|
392
|
+
if (regexp.source.indexOf(';') === -1) {
|
|
393
|
+
// we do essence check
|
|
394
|
+
return regexp.test(essenceMIMEType)
|
|
395
|
+
} else {
|
|
396
|
+
// when the content-type includes parameters
|
|
397
|
+
// we do a full-text match
|
|
398
|
+
return regexp.test(contentType)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function ParserListItem (contentType) {
|
|
403
|
+
this.name = contentType
|
|
404
|
+
// we pre-calculate all the needed information
|
|
405
|
+
// before content-type comparsion
|
|
406
|
+
const parsed = safeParseContentType(contentType)
|
|
407
|
+
this.type = parsed.type
|
|
408
|
+
this.parameters = parsed.parameters
|
|
409
|
+
this.parameterKeys = Object.keys(parsed.parameters)
|
|
410
|
+
this.isEssence = contentType.indexOf(';') === -1
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// used in ContentTypeParser.remove
|
|
414
|
+
ParserListItem.prototype.toString = function () {
|
|
415
|
+
return this.name
|
|
416
|
+
}
|
|
417
|
+
|
|
349
418
|
module.exports = ContentTypeParser
|
|
350
419
|
module.exports.helpers = {
|
|
351
420
|
buildContentTypeParser,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
|
|
3
|
+
"version": "4.10.2",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -176,6 +176,7 @@
|
|
|
176
176
|
"@fastify/fast-json-stringify-compiler": "^4.1.0",
|
|
177
177
|
"abstract-logging": "^2.0.1",
|
|
178
178
|
"avvio": "^8.2.0",
|
|
179
|
+
"content-type": "^1.0.4",
|
|
179
180
|
"find-my-way": "^7.3.0",
|
|
180
181
|
"light-my-request": "^5.6.1",
|
|
181
182
|
"pino": "^8.5.0",
|
|
@@ -395,3 +395,217 @@ test('Safeguard against malicious content-type / 3', async t => {
|
|
|
395
395
|
|
|
396
396
|
t.same(response.statusCode, 415)
|
|
397
397
|
})
|
|
398
|
+
|
|
399
|
+
test('Safeguard against content-type spoofing - string', async t => {
|
|
400
|
+
t.plan(1)
|
|
401
|
+
|
|
402
|
+
const fastify = Fastify()
|
|
403
|
+
fastify.removeAllContentTypeParsers()
|
|
404
|
+
fastify.addContentTypeParser('text/plain', function (request, body, done) {
|
|
405
|
+
t.pass('should be called')
|
|
406
|
+
done(null, body)
|
|
407
|
+
})
|
|
408
|
+
fastify.addContentTypeParser('application/json', function (request, body, done) {
|
|
409
|
+
t.fail('shouldn\'t be called')
|
|
410
|
+
done(null, body)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
fastify.post('/', async () => {
|
|
414
|
+
return 'ok'
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
await fastify.inject({
|
|
418
|
+
method: 'POST',
|
|
419
|
+
path: '/',
|
|
420
|
+
headers: {
|
|
421
|
+
'content-type': 'text/plain; content-type="application/json"'
|
|
422
|
+
},
|
|
423
|
+
body: ''
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('Safeguard against content-type spoofing - regexp', async t => {
|
|
428
|
+
t.plan(1)
|
|
429
|
+
|
|
430
|
+
const fastify = Fastify()
|
|
431
|
+
fastify.removeAllContentTypeParsers()
|
|
432
|
+
fastify.addContentTypeParser(/text\/plain/, function (request, body, done) {
|
|
433
|
+
t.pass('should be called')
|
|
434
|
+
done(null, body)
|
|
435
|
+
})
|
|
436
|
+
fastify.addContentTypeParser(/application\/json/, function (request, body, done) {
|
|
437
|
+
t.fail('shouldn\'t be called')
|
|
438
|
+
done(null, body)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
fastify.post('/', async () => {
|
|
442
|
+
return 'ok'
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
await fastify.inject({
|
|
446
|
+
method: 'POST',
|
|
447
|
+
path: '/',
|
|
448
|
+
headers: {
|
|
449
|
+
'content-type': 'text/plain; content-type="application/json"'
|
|
450
|
+
},
|
|
451
|
+
body: ''
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
test('content-type match parameters - string 1', async t => {
|
|
456
|
+
t.plan(1)
|
|
457
|
+
|
|
458
|
+
const fastify = Fastify()
|
|
459
|
+
fastify.removeAllContentTypeParsers()
|
|
460
|
+
fastify.addContentTypeParser('text/plain; charset=utf8', function (request, body, done) {
|
|
461
|
+
t.fail('shouldn\'t be called')
|
|
462
|
+
done(null, body)
|
|
463
|
+
})
|
|
464
|
+
fastify.addContentTypeParser('application/json; charset=utf8', function (request, body, done) {
|
|
465
|
+
t.pass('should be called')
|
|
466
|
+
done(null, body)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
fastify.post('/', async () => {
|
|
470
|
+
return 'ok'
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
await fastify.inject({
|
|
474
|
+
method: 'POST',
|
|
475
|
+
path: '/',
|
|
476
|
+
headers: {
|
|
477
|
+
'content-type': 'application/json; charset=utf8'
|
|
478
|
+
},
|
|
479
|
+
body: ''
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
test('content-type match parameters - string 2', async t => {
|
|
484
|
+
t.plan(1)
|
|
485
|
+
|
|
486
|
+
const fastify = Fastify()
|
|
487
|
+
fastify.removeAllContentTypeParsers()
|
|
488
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
489
|
+
t.pass('should be called')
|
|
490
|
+
done(null, body)
|
|
491
|
+
})
|
|
492
|
+
fastify.addContentTypeParser('text/plain; charset=utf8; foo=bar', function (request, body, done) {
|
|
493
|
+
t.fail('shouldn\'t be called')
|
|
494
|
+
done(null, body)
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
fastify.post('/', async () => {
|
|
498
|
+
return 'ok'
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
await fastify.inject({
|
|
502
|
+
method: 'POST',
|
|
503
|
+
path: '/',
|
|
504
|
+
headers: {
|
|
505
|
+
'content-type': 'application/json; foo=bar; charset=utf8'
|
|
506
|
+
},
|
|
507
|
+
body: ''
|
|
508
|
+
})
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test('content-type match parameters - regexp', async t => {
|
|
512
|
+
t.plan(1)
|
|
513
|
+
|
|
514
|
+
const fastify = Fastify()
|
|
515
|
+
fastify.removeAllContentTypeParsers()
|
|
516
|
+
fastify.addContentTypeParser(/application\/json; charset=utf8/, function (request, body, done) {
|
|
517
|
+
t.pass('should be called')
|
|
518
|
+
done(null, body)
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
fastify.post('/', async () => {
|
|
522
|
+
return 'ok'
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
await fastify.inject({
|
|
526
|
+
method: 'POST',
|
|
527
|
+
path: '/',
|
|
528
|
+
headers: {
|
|
529
|
+
'content-type': 'application/json; charset=utf8'
|
|
530
|
+
},
|
|
531
|
+
body: ''
|
|
532
|
+
})
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test('content-type fail when parameters not match - string 1', async t => {
|
|
536
|
+
t.plan(1)
|
|
537
|
+
|
|
538
|
+
const fastify = Fastify()
|
|
539
|
+
fastify.removeAllContentTypeParsers()
|
|
540
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
541
|
+
t.fail('shouldn\'t be called')
|
|
542
|
+
done(null, body)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
fastify.post('/', async () => {
|
|
546
|
+
return 'ok'
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
const response = await fastify.inject({
|
|
550
|
+
method: 'POST',
|
|
551
|
+
path: '/',
|
|
552
|
+
headers: {
|
|
553
|
+
'content-type': 'application/json; charset=utf8'
|
|
554
|
+
},
|
|
555
|
+
body: ''
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
t.same(response.statusCode, 415)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test('content-type fail when parameters not match - string 2', async t => {
|
|
562
|
+
t.plan(1)
|
|
563
|
+
|
|
564
|
+
const fastify = Fastify()
|
|
565
|
+
fastify.removeAllContentTypeParsers()
|
|
566
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
567
|
+
t.fail('shouldn\'t be called')
|
|
568
|
+
done(null, body)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
fastify.post('/', async () => {
|
|
572
|
+
return 'ok'
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
const response = await fastify.inject({
|
|
576
|
+
method: 'POST',
|
|
577
|
+
path: '/',
|
|
578
|
+
headers: {
|
|
579
|
+
'content-type': 'application/json; charset=utf8; foo=baz'
|
|
580
|
+
},
|
|
581
|
+
body: ''
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
t.same(response.statusCode, 415)
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
test('content-type fail when parameters not match - regexp', async t => {
|
|
588
|
+
t.plan(1)
|
|
589
|
+
|
|
590
|
+
const fastify = Fastify()
|
|
591
|
+
fastify.removeAllContentTypeParsers()
|
|
592
|
+
fastify.addContentTypeParser(/application\/json; charset=utf8; foo=bar/, function (request, body, done) {
|
|
593
|
+
t.fail('shouldn\'t be called')
|
|
594
|
+
done(null, body)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
fastify.post('/', async () => {
|
|
598
|
+
return 'ok'
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
const response = await fastify.inject({
|
|
602
|
+
method: 'POST',
|
|
603
|
+
path: '/',
|
|
604
|
+
headers: {
|
|
605
|
+
'content-type': 'application/json; charset=utf8'
|
|
606
|
+
},
|
|
607
|
+
body: ''
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
t.same(response.statusCode, 415)
|
|
611
|
+
})
|
|
@@ -1053,7 +1053,7 @@ test('The charset should not interfere with the content type handling', t => {
|
|
|
1053
1053
|
url: getUrl(fastify),
|
|
1054
1054
|
body: '{"hello":"world"}',
|
|
1055
1055
|
headers: {
|
|
1056
|
-
'Content-Type': 'application/json charset=utf-8'
|
|
1056
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
1057
1057
|
}
|
|
1058
1058
|
}, (err, response, body) => {
|
|
1059
1059
|
t.error(err)
|
|
@@ -1236,7 +1236,7 @@ test('contentTypeParser should add a custom parser with RegExp value', t => {
|
|
|
1236
1236
|
url: getUrl(fastify),
|
|
1237
1237
|
body: '{"hello":"world"}',
|
|
1238
1238
|
headers: {
|
|
1239
|
-
'Content-Type': 'weird
|
|
1239
|
+
'Content-Type': 'weird/content-type+json'
|
|
1240
1240
|
}
|
|
1241
1241
|
}, (err, response, body) => {
|
|
1242
1242
|
t.error(err)
|
|
@@ -1266,7 +1266,7 @@ test('contentTypeParser should add multiple custom parsers with RegExp values',
|
|
|
1266
1266
|
done(null, 'xml')
|
|
1267
1267
|
})
|
|
1268
1268
|
|
|
1269
|
-
fastify.addContentTypeParser(/.*\+myExtension
|
|
1269
|
+
fastify.addContentTypeParser(/.*\+myExtension$/i, function (req, payload, done) {
|
|
1270
1270
|
let data = ''
|
|
1271
1271
|
payload.on('data', chunk => { data += chunk })
|
|
1272
1272
|
payload.on('end', () => {
|