Skip to content

Commit 42b4759

Browse files
Fix #4979: Replace connect-multiparty with multer to resolve security vulnerability (#5013)
* Fix #4979: Replace connect-multiparty with multer to resolve security vulnerability - Remove connect-multiparty dependency (deprecated with CVE-2022-29623) - Add multer v1.4.5-lts.1 as replacement for file upload handling - Update attachment upload route to use multer.single('file') - Update rich-text CSV upload to use multer middleware - Migrate big-upload middleware to multer.any() for chunked uploads - Update test suite to handle multer file format - Maintain backward compatibility with file.name fallback - Move form-data to devDependencies as only needed for testing * Fix backwards compatibility for big-upload-middleware - Revert req.files from array back to object format to maintain connect-multiparty API - Use original property names (name, type) instead of multer names (originalname, mimetype) - Revert test changes since the public API should not change - This addresses @boutell's feedback about maintaining the documented interface * Clean up: Remove temporary test file and fix formatting - Remove test-big-upload-fix.js (temporary test file) - Clean up formatting in big-upload.js test * Upgrade multer to v2.0.2 to address security vulnerability - Update multer from v1.4.5-lts.1 (vulnerable) to v2.0.2 (secure) - Maintains full API compatibility with existing implementation - No code changes required - multer 2.x maintains same interface - Verified: npm audit shows 0 vulnerabilities Fixes: #5013 (review)
1 parent de6b323 commit 42b4759

4 files changed

Lines changed: 27 additions & 24 deletions

File tree

modules/@apostrophecms/attachment/index.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,12 @@ module.exports = {
159159
post: {
160160
upload: [
161161
self.canUpload,
162-
require('connect-multiparty')(),
162+
//In the existing code, we are reading the zeroth element from the files array object, which results in processing only a single file. Therefore, I am currently reading just one file from the Multer package.
163+
require('multer')({ dest: require('os').tmpdir() }).single('file'),
163164
async function (req) {
164165
try {
165-
// The name attribute could be anything because of how fileupload
166-
// controls work; we don't really care.
167-
const file = Object.values(req.files || {})[0];
166+
// The file comes from multer middleware
167+
const file = req.file;
168168

169169
if (!file) {
170170
throw self.apos.error('invalid');
@@ -175,11 +175,12 @@ module.exports = {
175175

176176
return attachment;
177177
} finally {
178-
for (const file of (Object.values(req.files || {}))) {
178+
//Hence I am reading the single file from the upload and I am checking the condtion for the same
179+
if (req.file) {
179180
try {
180-
fs.unlinkSync(file.path);
181+
fs.unlinkSync(req.file.path);
181182
} catch (e) {
182-
self.apos.util.warn(`Uploaded temporary file ${file.path} was already removed, this should have been the responsibility of the upload route`);
183+
self.apos.util.warn(`Uploaded temporary file ${req.file.path} was already removed, this should have been the responsibility of the upload route`);
183184
}
184185
}
185186
}
@@ -393,7 +394,9 @@ module.exports = {
393394
// object, suitable for passing to the `url` API and for use as the value
394395
// of a `type: 'attachment'` schema field.
395396
async insert(req, file, options = {}) {
396-
let extension = path.extname(file.name);
397+
// Handle both multer (originalname) and connect-multiparty (name) formats
398+
const fileName = file.originalname || file.name;
399+
let extension = path.extname(fileName);
397400
if (extension && extension.length) {
398401
extension = extension.substr(1);
399402
}
@@ -420,9 +423,9 @@ module.exports = {
420423
_id: options.attachmentId ?? self.apos.util.generateId(),
421424
group: group.name,
422425
createdAt: new Date(),
423-
name: self.apos.util.slugify(path.basename(file.name, path.extname(file.name))),
426+
name: self.apos.util.slugify(path.basename(fileName, path.extname(fileName))),
424427
title: self.apos.util.sortify(
425-
path.basename(file.name, path.extname(file.name))
428+
path.basename(fileName, path.extname(fileName))
426429
),
427430
extension,
428431
type: 'attachment',

modules/@apostrophecms/http/lib/big-upload-middleware.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const multiparty = require('connect-multiparty');
1+
const multer = require('multer');
22
const util = require('util');
33
const {
44
readFile, open, unlink
@@ -17,10 +17,10 @@ module.exports = (self) => ({
1717

1818
bigUploadMiddleware({ authorize } = {}) {
1919
return (req, res, next) => {
20-
// Chain the multiparty middleware to handle normal uploads
20+
// Chain the multer middleware to handle normal uploads
2121
// as chunks (more efficient than base64 etc)
22-
const multipartyFn = multiparty();
23-
return multipartyFn(req, res, () => {
22+
const multerFn = multer({ dest: require('os').tmpdir() }).any();
23+
return multerFn(req, res, () => {
2424
return body(req, res, next);
2525
});
2626
};
@@ -56,10 +56,10 @@ module.exports = (self) => ({
5656
});
5757
}
5858
} finally {
59-
// Clean up multiparty temporary files
60-
for (const { path } of Object.values(origFiles || {})) {
59+
// Clean up multer temporary files
60+
for (const file of (origFiles || [])) {
6161
try {
62-
await unlink(path);
62+
await unlink(file.path);
6363
} catch (e) {
6464
// OK if it is already gone
6565
}
@@ -136,7 +136,7 @@ module.exports = (self) => ({
136136
if ((chunk < 0) || (chunk >= info.chunks)) {
137137
throw self.apos.error('invalid', 'chunk out of range');
138138
}
139-
const file = req.files.chunk;
139+
const file = req.files.find(f => f.fieldname === 'chunk') || req.files[0];
140140
const ufs = self.getBigUploadFs();
141141
const ufsPath = `/big-uploads/${id}-${n}-${chunk}`;
142142
await ufs.copyIn(file.path, ufsPath);

modules/@apostrophecms/rich-text-widget/lib/apiRoutes.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs = require('node:fs');
2-
const connectMultiparty = require('connect-multiparty');
2+
const multer = require('multer');
33
const { pipeline } = require('stream/promises');
44
const { parse: csvParse } = require('csv-parse');
55
const { Transform } = require('stream');
@@ -9,14 +9,14 @@ module.exports = self => {
99
return {
1010
post: {
1111
generateCsvTable: [
12-
connectMultiparty(),
12+
multer({ dest: require('os').tmpdir() }).single('file'),
1313
async (req) => {
14-
const { file } = req.files || {};
14+
const file = req.file;
1515
if (!file) {
1616
throw self.apos.error('invalid', 'A file is required');
1717
}
1818

19-
const extension = file.name.split('.').pop();
19+
const extension = file.originalname.split('.').pop();
2020
if (extension !== 'csv') {
2121
throw self.apos.error('invalid', 'Only csv files are supported');
2222
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"chokidar": "^3.5.2",
6666
"common-tags": "^1.8.0",
6767
"connect-mongo": "^5.1.0",
68-
"connect-multiparty": "^2.1.1",
6968
"cookie-parser": "^1.4.5",
7069
"cors": "^2.8.5",
7170
"css-loader": "^5.2.4",
@@ -76,7 +75,6 @@
7675
"express-bearer-token": "^3.0.0",
7776
"express-cache-on-demand": "^1.0.3",
7877
"express-session": "^1.18.2",
79-
"form-data": "^4.0.0",
8078
"fs-extra": "^7.0.1",
8179
"glob": "^10.4.5",
8280
"he": "^1.2.0",
@@ -92,6 +90,7 @@
9290
"mini-css-extract-plugin": "^1.6.0",
9391
"minimatch": "^3.0.4",
9492
"mkdirp": "^0.5.5",
93+
"multer": "^2.0.2",
9594
"node-fetch": "^2.6.1",
9695
"nodemailer": "^6.6.1",
9796
"nunjucks": "^3.2.1",
@@ -134,6 +133,7 @@
134133
},
135134
"devDependencies": {
136135
"eslint-config-apostrophe": "^5.0.0",
136+
"form-data": "^4.0.4",
137137
"mocha": "^10.7.3",
138138
"nyc": "^15.1.0",
139139
"replace-in-file": "^6.1.0",

0 commit comments

Comments
 (0)