expressjs / multer

Node.js middleware for handling `multipart/form-data`.
MIT License
11.54k stars 1.05k forks source link

Uncaught `error` event from busboy can crash http servers #1176

Open max-mathieu opened 1 year ago

max-mathieu commented 1 year ago

There is a case of malformed requests that can take down a full nodejs app, due to an uncaught error event thrown by busboy

Using the latest 1.4.5-lts.1 from npm, the following code results in a full crash of the app (tested on both node 14 and node 18)

const express = require('express')
const multer  = require('multer')
const http  = require('http')
const upload = multer({ dest: 'uploads/' })
const port = 8888

const app = express()

app.post('/upload', upload.single('file'), function (req, res) {
  res.send({})
})

app.listen(port, () => {
  console.log(`Listening on port ${port}`)

  const boundary = 'AaB03x'
  const body = [
    '--' + boundary,
    'Content-Disposition: form-data; name="file"; filename="test.txt"',
    'Content-Type: text/plain',
    '',
    'test without end boundary'
  ].join('\r\n')
  const options = {
    hostname: 'localhost',
    port,
    path: '/upload',
    method: 'POST',
    headers: {
      'content-type': 'multipart/form-data; boundary=' + boundary,
      'content-length': body.length,
    }
  }
  const req = http.request(options, (res) => {
    console.log(res.statusCode)
  })
  req.on('error', (err) => {
    console.error(err)
  })
  req.write(body)
  req.end()
})

Result:

# node multer-bug.js
Listening on port 8888
events.js:377
      throw er; // Unhandled 'error' event
      ^

Error: Unexpected end of form
    at Multipart._final (/Users/max/src/tests/node_modules/busboy/lib/types/multipart.js:588:17)
    at callFinal (internal/streams/writable.js:610:10)
    at processTicksAndRejections (internal/process/task_queues.js:82:21)
Emitted 'error' event on Multipart instance at:
    at emitErrorNT (internal/streams/destroy.js:106:8)
    at emitErrorCloseNT (internal/streams/destroy.js:74:3)
    at processTicksAndRejections (internal/process/task_queues.js:82:21) {
  storageErrors: []
}
# back to prompt

The crash doesn't happen as soon as process.on('uncaughtException') is set, which probably explains why this test passes https://github.com/expressjs/multer/blob/lts/test/error-handling.js#L226-L250 with the same request.

This is not happening with multer 1.4.3 (and busboy pre-1.0)

I tracked this down to the call to busboy.removeAllListeners in https://github.com/expressjs/multer/blob/lts/lib/make-middleware.js#L40-L46 happening before busboy emits another error event (which I think comes from the _destroy call).

Since I feel like this removeAllListeners is mostly for cleanup/mem-leak prevention, I tried wrapping the removeAllListeners call with process.nextTick, and it does eliminate the issue.

I'm opening a PR with this fix, but I am unable to adjust the tests since they don't fail with the exact same payload

max-mathieu commented 1 year ago

For context, we had a client calling the API with malformed requests (I guess crafting a multipart/form-data request is hard in some languages...) which resulting in our API crashing and restarting, though interrupting other users' ongoing uploads

As such, this is a potential vector for a bad actor to take down apps with relatively minimal effort.

Only protection for now seems to have a process.on('uncaughtException') that filters out this specific exception (since it's best practice to not prevent the crash in the uncaughtException listener).

eamon0989 commented 11 months ago

We are facing the same issue. We use NestJs and while file uploads work in general, malformed requests crash the server. We discovered the issue recently during pen-testing. I tried adding:

      process.nextTick(() => {
        busboy.removeAllListeners()
      })

to node_modules/multer/lib/make-middleware.js as you did in your PR and I can confirm that it fixes the issue.

jakubkorczyk commented 1 month ago

I'm facing the same issue while using a malformed file. This crashes whole server so it's quite critical issue.

Here are some steps to reproduce:

const express = require('express')
const app = express()
const multer  = require('multer')
const port = 3000

const upload = multer({ dest: 'uploads/', onError: (err) => console.log(err) })

app.post('/hw', upload.single('file'), function (req, res, next) {

})

app.listen(port, () => {
    console.log(`Example app listening on port ${port}`)
})

Use this file to send the request: dataraw.zip

Request which crashes the server:

curl --verbose 'localhost:3000/hw'  -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' --data-binary @dataraw
Moltivie commented 1 month ago

Has somebody found any solution to this issue?

nandorcsupor-met commented 1 month ago

Has somebody found any solution to this issue?

Downgrade the package - only solution I found. then wait for: https://github.com/expressjs/multer/pull/1177 to be merged I guess

podplatnikm commented 1 month ago

How can you downgrade multer if the version determined by the @nestjs/platform-express (if using Nestjs) ?

nandorcsupor-met commented 1 month ago

How can you downgrade multer if the version determined by the @nestjs/platform-express (if using Nestjs) ?

Then downgrade that too ? I dont know, for me it wasnt determined by NestJs

podplatnikm commented 1 month ago

bug: unhandled error from a malformed request can crash the server - Unexpected end of form nestjs/nest#12415

Hey, did you find a way to handle this in nestjs?