expressjs / multer

Node.js middleware for handling `multipart/form-data`.
MIT License
11.63k stars 1.06k 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 1 year 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 4 months 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 4 months ago

Has somebody found any solution to this issue?

nandorcsupor-met commented 4 months 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 4 months ago

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

nandorcsupor-met commented 4 months 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 4 months 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?