CVE-2021-23597: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: This affects the package fastify-multipart before 5.3.1. By providing a name=constructor property it is still possible to crash the application. **Note:** This is a bypass of CVE-2020-8136 (https://security.snyk.io/vuln/SNYK-JS-FASTIFYMULTIPART-1290382).
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 7.5
Published: 2022-02-11T17:15Z
Repository: https://github.com/fastify/fastify-multipart
Language: JavaScript
Files changed: index.js, test/legacy/append-body.test.js, test/legacy/multipart.test.js
Commit date: 2022-02-11 10:44:05 +0100
Commit message: Merge pull request from GHSA-qh73-qc3p-rjv2


Diff: diff --git a/index.js b/index.js
index 3b926bb..7ce031c 100644
--- a/index.js
+++ b/index.js
@@ -58,12 +58,12 @@ function attachToBody (options, req, reply, next) {
     }
     next(err)
   }, options)
 
   mp.on('field', (key, value) => {
-    if (key === '__proto__') {
-      mp.destroy(new Error('__proto__ is not allowed as field name'))
+    if (key === '__proto__' || key === 'constructor') {
+      mp.destroy(new Error(`${key} is not allowed as field name`))
       return
     }
     if (body[key] === undefined) {
       body[key] = value
     } else if (Array.isArray(body[key])) {
@@ -255,12 +255,12 @@ function fastifyMultipart (fastify, options, done) {
 
     function wrap (field, file, filename, encoding, mimetype) {
       log.debug({ field, filename, encoding, mimetype }, 'parsing part')
       files++
       eos(file, waitForFiles)
-      if (field === '__proto__') {
-        file.destroy(new Error('__proto__ is not allowed as field name'))
+      if (field === '__proto__' || field === 'constructor') {
+        file.destroy(new Error(`${field} is not allowed as field name`))
         return
       }
       handler(field, file, filename, encoding, mimetype)
     }
 
diff --git a/test/legacy/append-body.test.js b/test/legacy/append-body.test.js
index fdfcec2..1c8331c 100644
--- a/test/legacy/append-body.test.js
+++ b/test/legacy/append-body.test.js
@@ -783,5 +783,51 @@ test('addToBody with __proto__ field', t => {
     pump(form, req, function (err) {
       t.error(err, 'client pump: no err')
     })
   })
 })
+
+test('addToBody with constructor field', t => {
+  t.plan(3)
+
+  const fastify = Fastify()
+  t.teardown(fastify.close.bind(fastify))
+
+  const opts = {
+    addToBody: true,
+    onFile: (fieldName, stream, filename, encoding, mimetype) => {
+      t.fail('there are not stream')
+    }
+  }
+  fastify.register(multipart, opts)
+
+  fastify.post('/', function (req, reply) {
+    t.fail('should not be called')
+  })
+
+  fastify.listen(0, 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')
+      })
+    })
+
+    form.append('myField', 'hello')
+    form.append('constructor', 'world')
+    pump(form, req, function (err) {
+      t.error(err, 'client pump: no err')
+    })
+  })
+})
diff --git a/test/legacy/multipart.test.js b/test/legacy/multipart.test.js
index eb56a85..b95444d 100644
--- a/test/legacy/multipart.test.js
+++ b/test/legacy/multipart.test.js
@@ -476,5 +476,57 @@ test('should not allow __proto__', { skip: process.platform === 'win32' }, funct
     pump(form, req, function (err) {
       t.error(err, 'client pump: no err')
     })
   })
 })
+
+test('should not allow constructor', { skip: process.platform === 'win32' }, function (t) {
+  t.plan(5)
+
+  const fastify = Fastify()
+  t.teardown(fastify.close.bind(fastify))
+
+  fastify.register(multipart, { limits: { fields: 1 } })
+
+  fastify.post('/', function (req, reply) {
+    t.ok(req.isMultipart())
+
+    const mp = req.multipart(handler, function (err) {
+      t.equal(err.message, 'constructor is not allowed as field name')
+      reply.code(500).send()
+    })
+
+    mp.on('field', function (name, value) {
+      t.fail('should not be called')
+    })
+
+    function handler (field, file, filename, encoding, mimetype) {
+      t.fail('should not be called')
+    }
+  })
+
+  fastify.listen(0, 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')
+      })
+    })
+    const rs = fs.createReadStream(filePath)
+    form.append('constructor', rs)
+    pump(form, req, function (err) {
+      t.error(err, 'client pump: no err')
+    })
+  })
+})

Vulnerable code:     if (key === '__proto__') {
      mp.destroy(new Error('__proto__ is not allowed as field name'))
      if (field === '__proto__') {
        file.destroy(new Error('__proto__ is not allowed as field name'))
Fixed code:     if (key === '__proto__' || key === 'constructor') {
      mp.destroy(new Error(`${key} is not allowed as field name`))
      if (field === '__proto__' || field === 'constructor') {
        file.destroy(new Error(`${field} is not allowed as field name`))

test('addToBody with constructor field', t => {
  t.plan(3)

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

  const opts = {
    addToBody: true,
    onFile: (fieldName, stream, filename, encoding, mimetype) => {
      t.fail('there are not stream')
    }
  }
  fastify.register(multipart, opts)

  fastify.post('/', function (req, reply) {
    t.fail('should not be called')
  })

  fastify.listen(0, 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')
      })
    })

    form.append('myField', 'hello')
    form.append('constructor', 'world')
    pump(form, req, function (err) {
      t.error(err, 'client pump: no err')
    })
  })
})

test('should not allow constructor', { skip: process.platform === 'win32' }, function (t) {
  t.plan(5)

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

  fastify.register(multipart, { limits: { fields: 1 } })

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

    const mp = req.multipart(handler, function (err) {
      t.equal(err.message, 'constructor is not allowed as field name')
      reply.code(500).send()
    })

    mp.on('field', function (name, value) {
      t.fail('should not be called')
    })

    function handler (field, file, filename, encoding, mimetype) {
      t.fail('should not be called')
    }
  })

  fastify.listen(0, 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')
      })
    })
    const rs = fs.createReadStream(filePath)
    form.append('constructor', rs)
    pump(form, req, function (err) {
      t.error(err, 'client pump: no err')
    })
  })
})