diff --git a/CHANGELOG.md b/CHANGELOG.md index 9686cde2..5aeb328f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 1.4.5-lts.1 + +- No changes +## 1.4.4-lts.1 + +- Bugfix: Bump busboy to fix CVE-2022-24434 (#1097) +- Breaking: Require Node.js 6.0.0 or later (#1097) + ## 1.4.4 - 2021-12-07 - Bugfix: Handle missing field names (#913) diff --git a/README.md b/README.md index 7f5d0807..2b885785 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ Key | Description `dest` or `storage` | Where to store the files `fileFilter` | Function to control which files are accepted `limits` | Limits of the uploaded data +`parseJsonFields` | Parse fields that have the content-type `application/json` `preservePath` | Keep the full path of files instead of just the base name In an average web app, only `dest` might be required, and configured as shown in @@ -299,6 +300,13 @@ function fileFilter (req, file, cb) { } ``` +### `parseJsonFields` + +Fields may also have a `Content-Type` header. If you set `parseJsonFields` to +`true` these fields will be parsed using `JSON.parse()` instead of handled as +plain text strings. This way you don't need to unroll complex JSON structures +that are transmitted alongside uploaded files as url-encoded fields. + ## Error handling When encountering an error, Multer will delegate the error to Express. You can diff --git a/index.js b/index.js index d5b67eba..5fa7fe4c 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ function Multer (options) { this.limits = options.limits this.preservePath = options.preservePath this.fileFilter = options.fileFilter || allowAll + this.parseJsonFields = !!options.parseJsonFields } Multer.prototype._makeMiddleware = function (fields, fileStrategy) { @@ -49,7 +50,8 @@ Multer.prototype._makeMiddleware = function (fields, fileStrategy) { preservePath: this.preservePath, storage: this.storage, fileFilter: wrappedFileFilter, - fileStrategy: fileStrategy + fileStrategy: fileStrategy, + parseJsonFields: this.parseJsonFields } } @@ -79,7 +81,8 @@ Multer.prototype.any = function () { preservePath: this.preservePath, storage: this.storage, fileFilter: this.fileFilter, - fileStrategy: 'ARRAY' + fileStrategy: 'ARRAY', + parseJsonFields: this.parseJsonFields } } diff --git a/lib/make-middleware.js b/lib/make-middleware.js index b033cbd9..477597d0 100644 --- a/lib/make-middleware.js +++ b/lib/make-middleware.js @@ -1,7 +1,6 @@ var is = require('type-is') var Busboy = require('busboy') var extend = require('xtend') -var onFinished = require('on-finished') var appendField = require('append-field') var Counter = require('./counter') @@ -9,10 +8,6 @@ var MulterError = require('./multer-error') var FileAppender = require('./file-appender') var removeUploadedFiles = require('./remove-uploaded-files') -function drainStream (stream) { - stream.on('readable', stream.read.bind(stream)) -} - function makeMiddleware (setup) { return function multerMiddleware (req, res, next) { if (!is(req, ['multipart'])) return next() @@ -24,13 +19,14 @@ function makeMiddleware (setup) { var fileFilter = options.fileFilter var fileStrategy = options.fileStrategy var preservePath = options.preservePath + var parseJsonFields = options.parseJsonFields req.body = Object.create(null) var busboy try { - busboy = new Busboy({ headers: req.headers, limits: limits, preservePath: preservePath }) + busboy = Busboy({ headers: req.headers, limits: limits, preservePath: preservePath }) } catch (err) { return next(err) } @@ -45,12 +41,9 @@ function makeMiddleware (setup) { function done (err) { if (isDone) return isDone = true - req.unpipe(busboy) - drainStream(req) busboy.removeAllListeners() - - onFinished(req, function () { next(err) }) + next(err) } function indicateDone () { @@ -80,9 +73,9 @@ function makeMiddleware (setup) { } // handle text field data - busboy.on('field', function (fieldname, value, fieldnameTruncated, valueTruncated) { + busboy.on('field', function (fieldname, value, { nameTruncated, valueTruncated, mimeType }) { if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME') - if (fieldnameTruncated) return abortWithCode('LIMIT_FIELD_KEY') + if (nameTruncated) return abortWithCode('LIMIT_FIELD_KEY') if (valueTruncated) return abortWithCode('LIMIT_FIELD_VALUE', fieldname) // Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6) @@ -90,11 +83,19 @@ function makeMiddleware (setup) { if (fieldname.length > limits.fieldNameSize) return abortWithCode('LIMIT_FIELD_KEY') } + if (parseJsonFields && mimeType === 'application/json') { + try { + value = JSON.parse(value) + } catch (error) { + return abortWithError(error) + } + } + appendField(req.body, fieldname, value) }) // handle files - busboy.on('file', function (fieldname, fileStream, filename, encoding, mimetype) { + busboy.on('file', function (fieldname, fileStream, { filename, encoding, mimeType }) { // don't attach to the files object, if there is no file if (!filename) return fileStream.resume() @@ -107,7 +108,7 @@ function makeMiddleware (setup) { fieldname: fieldname, originalname: filename, encoding: encoding, - mimetype: mimetype + mimetype: mimeType } var placeholder = appender.insertPlaceholder(file) @@ -169,7 +170,7 @@ function makeMiddleware (setup) { busboy.on('partsLimit', function () { abortWithCode('LIMIT_PART_COUNT') }) busboy.on('filesLimit', function () { abortWithCode('LIMIT_FILE_COUNT') }) busboy.on('fieldsLimit', function () { abortWithCode('LIMIT_FIELD_COUNT') }) - busboy.on('finish', function () { + busboy.on('close', function () { readFinished = true indicateDone() }) diff --git a/package.json b/package.json index 6aec9996..8545a73d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "multer", "description": "Middleware for handling `multipart/form-data`.", - "version": "1.4.4", + "version": "1.4.5-lts.1", "contributors": [ "Hage Yaapa (http://www.hacksparrow.com)", "Jaret Pfluger ", @@ -20,11 +20,10 @@ ], "dependencies": { "append-field": "^1.0.0", - "busboy": "^0.2.11", + "busboy": "^1.0.0", "concat-stream": "^1.5.2", "mkdirp": "^0.5.4", "object-assign": "^4.1.1", - "on-finished": "^2.3.0", "type-is": "^1.6.4", "xtend": "^4.0.0" }, @@ -39,7 +38,7 @@ "testdata-w3c-json-form": "^1.0.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 6.0.0" }, "files": [ "LICENSE", diff --git a/test/_util.js b/test/_util.js index e62bdafc..a6782ad8 100644 --- a/test/_util.js +++ b/test/_util.js @@ -1,7 +1,6 @@ var fs = require('fs') var path = require('path') var stream = require('stream') -var onFinished = require('on-finished') exports.file = function file (name) { return fs.createReadStream(path.join(__dirname, 'files', name)) @@ -17,11 +16,6 @@ exports.submitForm = function submitForm (multer, form, cb) { var req = new stream.PassThrough() - req.complete = false - form.once('end', function () { - req.complete = true - }) - form.pipe(req) req.headers = { 'content-type': 'multipart/form-data; boundary=' + form.getBoundary(), @@ -29,7 +23,7 @@ exports.submitForm = function submitForm (multer, form, cb) { } multer(req, null, function (err) { - onFinished(req, function () { cb(err, req) }) + cb(err, req) }) }) } diff --git a/test/error-handling.js b/test/error-handling.js index b462e16e..6baad5ab 100644 --- a/test/error-handling.js +++ b/test/error-handling.js @@ -244,7 +244,7 @@ describe('Error Handling', function () { req.end(body) upload(req, null, function (err) { - assert.strictEqual(err.message, 'Unexpected end of multipart data') + assert.strictEqual(err.message, 'Unexpected end of form') done() }) }) diff --git a/test/express-integration.js b/test/express-integration.js index 87ab8869..0cda05d4 100644 --- a/test/express-integration.js +++ b/test/express-integration.js @@ -8,7 +8,6 @@ var util = require('./_util') var express = require('express') var FormData = require('form-data') var concat = require('concat-stream') -var onFinished = require('on-finished') var port = 34279 @@ -27,7 +26,7 @@ describe('Express Integration', function () { req.on('response', function (res) { res.on('error', cb) res.pipe(concat({ encoding: 'buffer' }, function (body) { - onFinished(req, function () { cb(null, res, body) }) + cb(null, res, body) })) }) } diff --git a/test/unicode.js b/test/unicode.js index b7f98214..851b0ebc 100644 --- a/test/unicode.js +++ b/test/unicode.js @@ -2,12 +2,10 @@ var assert = require('assert') -var path = require('path') -var util = require('./_util') var multer = require('../') var temp = require('fs-temp') var rimraf = require('rimraf') -var FormData = require('form-data') +var stream = require('stream') describe('Unicode', function () { var uploadDir, upload @@ -34,21 +32,29 @@ describe('Unicode', function () { }) it('should handle unicode filenames', function (done) { - var form = new FormData() - var parser = upload.single('small0') - var filename = '\ud83d\udca9.dat' - - form.append('small0', util.file('small0.dat'), { filename: filename }) - - util.submitForm(parser, form, function (err, req) { + var req = new stream.PassThrough() + var boundary = 'AaB03x' + var body = [ + '--' + boundary, + 'Content-Disposition: form-data; name="small0"; filename="poo.dat"; filename*=utf-8\'\'%F0%9F%92%A9.dat', + 'Content-Type: text/plain', + '', + 'test with unicode filename', + '--' + boundary + '--' + ].join('\r\n') + + req.headers = { + 'content-type': 'multipart/form-data; boundary=' + boundary, + 'content-length': body.length + } + + req.end(body) + + upload.single('small0')(req, null, function (err) { assert.ifError(err) - assert.strictEqual(path.basename(req.file.path), filename) - assert.strictEqual(req.file.originalname, filename) - + assert.strictEqual(req.file.originalname, '\ud83d\udca9.dat') assert.strictEqual(req.file.fieldname, 'small0') - assert.strictEqual(req.file.size, 1778) - assert.strictEqual(util.fileSize(req.file.path), 1778) done() })