CVE-2023-25576 (published 2023-02-14T16:15Z)

@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.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-770
cwe_name: Allocation of Resources Without Limits or Throttling
description: 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.
extended_description: Code frequently has to work with limited resources, so programmers must be careful to ensure that resources are not consumed too quickly, or too easily.  Without use of quotas, resource limits, or other protection mechanisms, it can be easy for an attacker to consume many resources by rapidly making many requests, or causing larger resources to be used than is needed. When too many resources are allocated, or if a single resource is too large, then it can prevent the code from working correctly, possibly leading to a denial of service.
         

Fix information:
repo_url: https://github.com/fastify/fastify-multipart
commit_details: repo_url: https://github.com/fastify/fastify-multipart
author: Rafael Gonzaga
author_date: 2023-02-14 07:15:48-03:00
msg: 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
repository: repo_url: https://github.com/fastify/fastify-multipart
repo_name: fastify/fastify-multipart
description: Multipart support for Fastify
repo_language: JavaScript
owner: fastify
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -84,10 +84,13 @@ fastify.register(require('@fastify/multipart'), {
     fileSize: 1000000,  // For multipart forms, the max file size in bytes
     files: 1,           // Max number of file fields
     headerPairs: 2000   // Max number of header key=>value pairs
+    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.
+
 **Note**: if the file stream that is provided by `data.file` is not consumed, like in the example below with the usage of pump, the promise will not be fulfilled at the end of the multipart processing.
 This behavior is inherited from [`@fastify/busboy`](https://github.com/fastify/busboy).
 

programming_language: Markdown
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -110,6 +110,12 @@ function busboy (options) {
 }
 
 function fastifyMultipart (fastify, options, done) {
+  options.limits = {
+    ...options.limits,
+    parts: options.limits?.parts || 1000,
+    fileSize: options.limits?.fileSize || fastify.initialConfig.bodyLimit
+  }
+
   const attachFieldsToBody = options.attachFieldsToBody
   if (options.addToBody === true) {
     if (typeof options.sharedSchemaId === 'string') {
@@ -360,15 +366,21 @@ function fastifyMultipart (fastify, options, done) {
       .on('finish', onEnd)
 
     bb.on('partsLimit', function () {
-      onError(new PartsLimitError())
+      const err = new PartsLimitError()
+      onError(err)
+      process.nextTick(() => onEnd(err))
     })
 
     bb.on('filesLimit', function () {
-      onError(new FilesLimitError())
+      const err = new FilesLimitError()
+      onError(err)
+      process.nextTick(() => onEnd(err))
     })
 
     bb.on('fieldsLimit', function () {
-      onError(new FieldsLimitError())
+      const err = new FieldsLimitError()
+      onError(err)
+      process.nextTick(() => onEnd(err))
     })
 
     request.pipe(bb)

programming_language: JavaScript
filename: big.test.js
change_type: ModificationType.MODIFY
diff: @@ -20,7 +20,12 @@ test('should upload a big file in constant memory', { skip: process.env.CI }, fu
 
   t.teardown(fastify.close.bind(fastify))
 
-  fastify.register(multipart)
+  fastify.register(multipart, {
+    limits: {
+      fileSize: Infinity,
+      parts: Infinity
+    }
+  })
 
   fastify.post('/', async function (req, reply) {
     t.ok(req.isMultipart())

programming_language: JavaScript
filename: big.test.js
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,12 @@ test('should upload a big file in constant memory', { skip: process.env.CI }, fu
 
   t.teardown(fastify.close.bind(fastify))
 
-  fastify.register(multipart)
+  fastify.register(multipart, {
+    limits: {
+      fileSize: Infinity,
+      parts: Infinity
+    }
+  })
 
   fastify.post('/', function (req, reply) {
     t.ok(req.isMultipart())

programming_language: JavaScript
filename: multipart-security.test.js
change_type: ModificationType.MODIFY
diff: @@ -7,6 +7,9 @@ const multipart = require('..')
 const http = require('http')
 const path = require('path')
 const fs = require('fs')
+const crypto = require('crypto')
+const EventEmitter = require('events')
+const { once } = EventEmitter
 
 const filePath = path.join(__dirname, '../README.md')
 
@@ -100,3 +103,106 @@ test('should not allow __proto__ as field name', function (t) {
     form.pipe(req)
   })
 })
+
+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)
+  })
+})

programming_language: JavaScript