CVE-2023-25576 (JavaScript, published 2023-02-14T16:15Z)
Description: value: @fastify/multipart is a Fastify plugin to parse the multipart content-type. Prior to versions 7.4.1 and 6.0.1, @fastify/multipart may experience denial of service due to a number of situations in which an unlimited number of parts are accepted. This includes the multipart body parser accepting an unlimited number of file parts, the multipart body parser accepting an unlimited number of field parts, and the multipart body parser accepting an unlimited number of empty parts as field parts. This is fixed in v7.4.1 (for Fastify v4.x) and v6.0.1 (for Fastify v3.x). There are no known workarounds.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-770 \u2014 Allocation of Resources Without Limits or Throttling
The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
Repository: https://github.com/fastify/fastify-multipart
Files changed:
README.md
index.js
test/big.test.js
test/legacy/big.test.js
test/multipart-security.test.js
Commit message: Merge pull request from GHSA-hpp2-2cr5-pf6g

* lib: implement default values for parts and fileSize

* fixup! lib: implement default values for parts and fileSize

Vulnerable code:
      onError(new PartsLimitError())
      onError(new FilesLimitError())
      onError(new FieldsLimitError())
  fastify.register(multipart)
  fastify.register(multipart)

Fixed code:
    parts: 1000         // For multipart forms, the max number of parts (fields + files)
For security reasons, `@fastify/multipart` sets the limit for `parts` and `fileSize` being _1000_ and _1048576_ respectively.

  options.limits = {
    ...options.limits,
    parts: options.limits?.parts || 1000,
    fileSize: options.limits?.fileSize || fastify.initialConfig.bodyLimit
  }

      const err = new PartsLimitError()
      onError(err)
      process.nextTick(() => onEnd(err))
      const err = new FilesLimitError()
      onError(err)
      process.nextTick(() => onEnd(err))
      const err = new FieldsLimitError()
      onError(err)
      process.nextTick(() => onEnd(err))
  fastify.register(multipart, {
    limits: {
      fileSize: Infinity,
      parts: Infinity
    }
  })
  fastify.register(multipart, {
    limits: {
      fileSize: Infinity,
      parts: Infinity
    }
  })
const crypto = require('crypto')
const EventEmitter = require('events')
const { once } = EventEmitter

test('should use default for fileSize', async function (t) {
  t.plan(4)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart, {
    throwFileSizeLimit: true
  })

  fastify.post('/', async function (req, reply) {
    t.ok(req.isMultipart())

    const part = await req.file()
    t.pass('the file is not consumed yet')

    try {
      await part.toBuffer()
      t.fail('it should throw')
    } catch (error) {
      t.ok(error)
      reply.send(error)
    }
  })

  await fastify.listen({ port: 0 })

  // request
  const form = new FormData()
  const opts = {
    hostname: '127.0.0.1',
    port: fastify.server.address().port,
    path: '/',
    headers: form.getHeaders(),
    method: 'POST'
  }

  const randomFileBuffer = Buffer.alloc(15_000_000)
  crypto.randomFillSync(randomFileBuffer)

  const req = http.request(opts)
  form.append('upload', randomFileBuffer)

  form.pipe(req)

  try {
    const [res] = await once(req, 'response')
    t.equal(res.statusCode, 413)
    res.resume()
    await once(res, 'end')
  } catch (error) {
    t.error(error, 'request')
  }
})

test('should use default for parts - 1000', function (t) {
  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart)

  fastify.post('/', async function (req, reply) {
    try {
      // eslint-disable-next-lint no-empty
      for await (const _ of req.parts()) { console.assert(_) }
      t.fail('should throw on 1001')
      reply.code(200).send()
    } catch (error) {
      t.ok(error instanceof fastify.multipartErrors.PartsLimitError)
      reply.code(500).send()
    }
  })

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, (res) => {
      t.equal(res.statusCode, 500)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
        t.end()
      })
    })
    for (let i = 0; i < 1000; ++i) {
      form.append('hello' + i, 'world')
    }
    // Exceeds the default limit (1000)
    form.append('hello', 'world')

    form.pipe(req)
  })
})