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 CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.10.1'
3
+ const VERSION = '4.10.2'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -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 parserName = this.parserList[i]
103
- if (contentType.indexOf(parserName) !== -1) {
104
- const parser = this.customParsers.get(parserName)
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 (parserRegExp.test(contentType)) {
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
- "version": "4.10.1",
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-content-type+json'
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$/, function (req, payload, done) {
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', () => {