fastify 3.29.3 → 3.29.5

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 = '3.29.3'
3
+ const VERSION = '3.29.5'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -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 { safeParse: safeParseContentType, defaultContentType } = require('fast-content-type-parse')
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,8 +70,9 @@ 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 {
75
+ contentType.isEssence = contentType.source.indexOf(';') === -1
74
76
  this.parserRegExpList.unshift(contentType)
75
77
  }
76
78
  this.customParsers.set(contentType.toString(), parser)
@@ -97,11 +99,25 @@ ContentTypeParser.prototype.getParser = function (contentType) {
97
99
  return this.customParsers.get(contentType)
98
100
  }
99
101
 
102
+ const parser = this.cache.get(contentType)
103
+ // TODO not covered by tests, this is a security backport
104
+ /* istanbul ignore next */
105
+ if (parser !== undefined) return parser
106
+
107
+ const parsed = safeParseContentType(contentType)
108
+
109
+ // dummyContentType always the same object
110
+ // we can use === for the comparsion and return early
111
+ if (parsed === defaultContentType) {
112
+ return this.customParsers.get('')
113
+ }
114
+
100
115
  // eslint-disable-next-line no-var
101
116
  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)
117
+ const parserListItem = this.parserList[i]
118
+ if (compareContentType(parsed, parserListItem)) {
119
+ const parser = this.customParsers.get(parserListItem.name)
120
+ // we set request content-type in cache to reduce parsing of MIME type
105
121
  this.cache.set(contentType, parser)
106
122
  return parser
107
123
  }
@@ -110,8 +126,9 @@ ContentTypeParser.prototype.getParser = function (contentType) {
110
126
  // eslint-disable-next-line no-var
111
127
  for (var j = 0; j !== this.parserRegExpList.length; ++j) {
112
128
  const parserRegExp = this.parserRegExpList[j]
113
- if (parserRegExp.test(contentType)) {
129
+ if (compareRegExpContentType(contentType, parsed.type, parserRegExp)) {
114
130
  const parser = this.customParsers.get(parserRegExp.toString())
131
+ // we set request content-type in cache to reduce parsing of MIME type
115
132
  this.cache.set(contentType, parser)
116
133
  return parser
117
134
  }
@@ -297,6 +314,7 @@ function buildContentTypeParser (c) {
297
314
  contentTypeParser[kDefaultJsonParse] = c[kDefaultJsonParse]
298
315
  contentTypeParser.customParsers = new Map(c.customParsers.entries())
299
316
  contentTypeParser.parserList = c.parserList.slice()
317
+ contentTypeParser.parserRegExpList = c.parserRegExpList.slice()
300
318
  return contentTypeParser
301
319
  }
302
320
 
@@ -348,6 +366,52 @@ function removeAllContentTypeParsers () {
348
366
  this[kContentTypeParser].removeAll()
349
367
  }
350
368
 
369
+ function compareContentType (contentType, parserListItem) {
370
+ if (parserListItem.isEssence) {
371
+ // we do essence check
372
+ return contentType.type.indexOf(parserListItem) !== -1
373
+ } else {
374
+ // when the content-type includes parameters
375
+ // we do a full-text search
376
+ // reject essence content-type before checking parameters
377
+ if (contentType.type.indexOf(parserListItem.type) === -1) return false
378
+ for (const key of parserListItem.parameterKeys) {
379
+ // reject when missing parameters
380
+ if (!(key in contentType.parameters)) return false
381
+ // reject when parameters do not match
382
+ if (contentType.parameters[key] !== parserListItem.parameters[key]) return false
383
+ }
384
+ return true
385
+ }
386
+ }
387
+
388
+ function compareRegExpContentType (contentType, essenceMIMEType, regexp) {
389
+ if (regexp.isEssence) {
390
+ // we do essence check
391
+ return regexp.test(essenceMIMEType)
392
+ } else {
393
+ // when the content-type includes parameters
394
+ // we do a full-text match
395
+ return regexp.test(contentType)
396
+ }
397
+ }
398
+
399
+ function ParserListItem (contentType) {
400
+ this.name = contentType
401
+ // we pre-calculate all the needed information
402
+ // before content-type comparsion
403
+ const parsed = safeParseContentType(contentType)
404
+ this.type = parsed.type
405
+ this.parameters = parsed.parameters
406
+ this.parameterKeys = Object.keys(parsed.parameters)
407
+ this.isEssence = contentType.indexOf(';') === -1
408
+ }
409
+
410
+ // used in ContentTypeParser.remove
411
+ ParserListItem.prototype.toString = function () {
412
+ return this.name
413
+ }
414
+
351
415
  module.exports = ContentTypeParser
352
416
  module.exports.helpers = {
353
417
  buildContentTypeParser,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "3.29.3",
3
+ "version": "3.29.5",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -158,9 +158,9 @@
158
158
  "ienoopen": "^1.1.0",
159
159
  "JSONStream": "^1.3.5",
160
160
  "license-checker": "^25.0.1",
161
- "pem": "^1.14.4",
162
161
  "proxyquire": "^2.1.3",
163
162
  "pump": "^3.0.0",
163
+ "self-cert": "^2.0.0",
164
164
  "send": "^0.17.1",
165
165
  "serve-static": "^1.14.1",
166
166
  "simple-get": "^4.0.0",
@@ -181,6 +181,7 @@
181
181
  "@fastify/error": "^2.0.0",
182
182
  "abstract-logging": "^2.0.0",
183
183
  "avvio": "^7.1.2",
184
+ "fast-content-type-parse": "^1.0.0",
184
185
  "fast-json-stringify": "^2.5.2",
185
186
  "find-my-way": "^4.5.0",
186
187
  "flatstr": "^1.0.12",
@@ -1,19 +1,18 @@
1
1
  'use strict'
2
2
 
3
- const util = require('util')
4
- const pem = require('pem')
5
-
6
- const createCertificate = util.promisify(pem.createCertificate)
3
+ const selfCert = require('self-cert')
7
4
 
8
5
  async function buildCertificate () {
9
6
  // "global" is used in here because "t.context" is only supported by "t.beforeEach" and "t.afterEach"
10
7
  // For the test case which execute this code which will be using `t.before` and it can reduce the
11
8
  // number of times executing it.
12
9
  if (!global.context || !global.context.cert || !global.context.key) {
13
- const keys = await createCertificate({ days: 1, selfSigned: true })
10
+ const certs = selfCert({
11
+ expires: new Date(Date.now() + 86400000)
12
+ })
14
13
  global.context = {
15
- cert: keys.certificate,
16
- key: keys.serviceKey
14
+ cert: certs.certificate,
15
+ key: certs.privateKey
17
16
  }
18
17
  }
19
18
  }
@@ -328,3 +328,260 @@ 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
+ })
545
+
546
+ // Refs: https://github.com/fastify/fastify/issues/4495
547
+ test('content-type regexp list should be cloned when plugin override', async t => {
548
+ t.plan(6)
549
+
550
+ const fastify = Fastify()
551
+
552
+ fastify.addContentTypeParser(/^image\/.*/, { parseAs: 'buffer' }, (req, payload, done) => {
553
+ done(null, payload)
554
+ })
555
+
556
+ fastify.register(function plugin (fastify, options, done) {
557
+ fastify.post('/', function (request, reply) {
558
+ reply.type(request.headers['content-type']).send(request.body)
559
+ })
560
+
561
+ done()
562
+ })
563
+
564
+ {
565
+ const { payload, headers, statusCode } = await fastify.inject({
566
+ method: 'POST',
567
+ path: '/',
568
+ payload: 'jpeg',
569
+ headers: { 'content-type': 'image/jpeg' }
570
+ })
571
+ t.same(statusCode, 200)
572
+ t.same(headers['content-type'], 'image/jpeg')
573
+ t.same(payload, 'jpeg')
574
+ }
575
+
576
+ {
577
+ const { payload, headers, statusCode } = await fastify.inject({
578
+ method: 'POST',
579
+ path: '/',
580
+ payload: 'png',
581
+ headers: { 'content-type': 'image/png' }
582
+ })
583
+ t.same(statusCode, 200)
584
+ t.same(headers['content-type'], 'image/png')
585
+ t.same(payload, 'png')
586
+ }
587
+ })
@@ -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-content-type+json'
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$/, function (req, payload, done) {
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', () => {