CVE-2022-39288 (JavaScript, published 2022-10-10T21:15Z)
Description: value: fastify is a fast and low overhead web framework, for Node.js. Affected versions of fastify are subject to a denial of service via malicious use of the Content-Type header. An attacker can send an invalid Content-Type header that can cause the application to crash. This issue has been addressed in commit `fbb07e8d` and will be included in release version 4.8.1. Users are advised to upgrade. Users unable to upgrade may manually filter out http content with malicious Content-Type headers.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-754 \u2014 Improper Check for Unusual or Exceptional Conditions
The product does not check or incorrectly checks for unusual or exceptional conditions that are not expected to occur frequently during day to day operation of the product.
Repository: https://github.com/fastify/fastify
Files changed:
lib/contentTypeParser.js
test/content-parser.test.js
Commit message: Merge pull request from GHSA-455w-c45v-86rg

* Add safeguard against malicious content types

* Update lib/contentTypeParser.js

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>

* Apply spelling fixes from code review

Co-authored-by: James Sumners <james@sumners.email>

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>
Co-authored-by: Frazer Smith <frazer.dev@outlook.com>
Co-authored-by: James Sumners <james@sumners.email>

Vulnerable code:
  this.customParsers = {}
  this.customParsers['application/json'] = new Parser(true, false, bodyLimit, this[kDefaultJsonParse])
  this.customParsers['text/plain'] = new Parser(true, false, bodyLimit, defaultPlainTextParser)
    this.customParsers[''] = parser
    this.customParsers[contentType] = parser
  return contentType in this.customParsers
  if (contentType === 'application/json') {
    return this.customParsers['application/json'] && this.customParsers['application/json'].fn !== this[kDefaultJsonParse]
  if (contentType === 'text/plain') {
    return this.customParsers['text/plain'] && this.customParsers['text/plain'].fn !== defaultPlainTextParser
  return contentType in this.customParsers
  if (contentType in this.customParsers) {
    return this.customParsers[contentType]
      const parser = this.customParsers[parserName]
      const parser = this.customParsers[parserRegExp]
  return this.customParsers['']
  this.customParsers = {}
  delete this.customParsers[contentType]
  Object.assign(contentTypeParser.customParsers, c.customParsers)
    t.equal(contentTypeParser.customParsers[''].fn, first)
    t.same(Object.keys(contentTypeParser.customParsers).length, 2)

Fixed code:
  // using a map instead of a plain object to avoid prototype hijack attacks
  this.customParsers = new Map()
  this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
  this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
    this.customParsers.set('', parser)
    this.customParsers.set(contentType.toString(), parser)
  return this.customParsers.has(typeof contentType === 'string' ? contentType : contentType.toString())
  if (contentType === 'application/json' && this.customParsers.has(contentType)) {
    return this.customParsers.get(contentType).fn !== this[kDefaultJsonParse]
  if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
    return this.customParsers.get(contentType).fn !== defaultPlainTextParser
  return this.hasParser(contentType)
  if (this.hasParser(contentType)) {
    return this.customParsers.get(contentType)
      const parser = this.customParsers.get(parserName)
      const parser = this.customParsers.get(parserRegExp.toString())
  return this.customParsers.get('')
  this.customParsers = new Map()
  this.customParsers.delete(contentType.toString())
  contentTypeParser.customParsers = new Map(c.customParsers.entries())
    t.equal(contentTypeParser.customParsers.get('').fn, first)
    t.same(contentTypeParser.customParsers.size, 2)

test('Safeguard against malicious content-type / 1', async t => {
  const badNames = Object.getOwnPropertyNames({}.__proto__) // eslint-disable-line
  t.plan(badNames.length)

  const fastify = Fastify()

  fastify.post('/', async () => {
    return 'ok'
  })

  for (const prop of badNames) {
    const response = await fastify.inject({
      method: 'POST',
      path: '/',
      headers: {
        'content-type': prop
      },
      body: ''
    })

    t.same(response.statusCode, 415)
  }
})

test('Safeguard against malicious content-type / 2', async t => {
  t.plan(1)

  const fastify = Fastify()

  fastify.post('/', async () => {
    return 'ok'
  })

  const response = await fastify.inject({
    method: 'POST',
    path: '/',
    headers: {
      'content-type': '\\u0063\\u006fnstructor'
    },
    body: ''
  })

  t.same(response.statusCode, 415)
})

test('Safeguard against malicious content-type / 3', async t => {
  t.plan(1)

  const fastify = Fastify()

  fastify.post('/', async () => {
    return 'ok'
  })

  const response = await fastify.inject({
    method: 'POST',
    path: '/',
    headers: {
      'content-type': 'constructor; charset=utf-8'
    },
    body: ''
  })

  t.same(response.statusCode, 415)
})