From 024ffed1ca5e87a5c1b7ef2bc3b94756945ac771 Mon Sep 17 00:00:00 2001 From: Paul Nothaft <53005142+the-luap@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:21:34 +0200 Subject: [PATCH] fix(backend): bump sharp, nodemailer, multer, js-yaml, joi for security fixes (stable) (#1375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): bump sharp, nodemailer, multer, js-yaml, joi for security fixes Backport of the same dependency bump on main (#1374). Resolves the same 12 code-scanning alerts flagged on stable's backend deps: sharp libheif RCE, nodemailer address-parser ReDoS + domain-validation bypasses, multer upload DoS/race conditions, js-yaml parsing DoS, and joi prototype pollution. All patch/minor bumps within the currently used major version. * fix(backend): set multer's fieldArrayIndexLimit to actually close CVE-2026-82333 The advisory is explicit that the 2.3.0 version bump alone doesn't remediate the array-index DoS — an app must also set limits.fieldArrayIndexLimit. Set it on every multer instance, sized to what each route's form actually needs. --------- Co-authored-by: Paul Nothaft --- .../__tests__/routes/publicContracts.test.js | 49 ++++ backend/package-lock.json | 270 +++++++++--------- backend/package.json | 4 +- backend/src/config/multerConfig.js | 18 +- backend/src/middleware/errorHandler.js | 10 + backend/src/routes/adminBackup.js | 6 +- backend/src/routes/adminBusinessProfile.js | 4 +- backend/src/routes/adminCMS.js | 4 +- backend/src/routes/adminContracts.js | 4 +- backend/src/routes/adminEvents/logo.js | 4 +- backend/src/routes/adminExpenses.js | 5 +- backend/src/routes/adminInvoices.js | 4 +- backend/src/routes/adminPhotos.js | 7 +- backend/src/routes/adminSettings.js | 9 +- backend/src/routes/gallery.js | 7 +- backend/src/routes/publicContracts.js | 5 +- backend/src/routes/v1/events.js | 4 +- 17 files changed, 260 insertions(+), 154 deletions(-) diff --git a/backend/__tests__/routes/publicContracts.test.js b/backend/__tests__/routes/publicContracts.test.js index c258f296..47d3ad7f 100644 --- a/backend/__tests__/routes/publicContracts.test.js +++ b/backend/__tests__/routes/publicContracts.test.js @@ -25,14 +25,18 @@ process.env.STORAGE_PATH = path.join(tmpDir, 'storage'); fs.mkdirSync(process.env.STORAGE_PATH, { recursive: true }); process.env.JWT_SECRET = process.env.JWT_SECRET || 'crm-route-test-secret'; +const express = require('express'); +const cookieParser = require('cookie-parser'); const request = require('supertest'); const { bootCrmDb, seedMinimal, createPublicToken, buildRouteApp } = require('../integration/helpers/crmDb'); const tokenGuards = require('../../src/utils/publicTokenGuards'); +const { errorHandler } = require('../../src/middleware/errorHandler'); describe('publicContracts routes', () => { let db; let cleanup; let app; + let appWithErrorHandler; let customerId; let contractId; @@ -51,6 +55,17 @@ describe('publicContracts routes', () => { contractId = inserted[0]?.id ?? inserted[0]; app = buildRouteApp('/api/public/contracts', require('../../src/routes/publicContracts')); + + // A second app instance wired to the REAL production error handler + // (buildRouteApp's is a simplified stand-in that only reads + // err.statusCode/err.status, which a bare MulterError doesn't set). + // Used below to verify the actual 4xx contract end-to-end, not just + // that multer aborted the request. + appWithErrorHandler = express(); + appWithErrorHandler.use(express.json()); + appWithErrorHandler.use(cookieParser()); + appWithErrorHandler.use('/api/public/contracts', require('../../src/routes/publicContracts')); + appWithErrorHandler.use(errorHandler); }, 120000); afterAll(async () => { @@ -131,6 +146,40 @@ describe('publicContracts routes', () => { .attach('file', Buffer.from('%PDF-1.4 fake'), 'signed.pdf'); expect(res.status).toBe(404); }); + + // CVE-2026-82333 regression (#1374 follow-up): multer 2.3.0 added an + // opt-in `fieldArrayIndexLimit` that must be set to actually close the + // field-parser DoS — the version bump alone does nothing. This route is + // unauthenticated (token-in-URL only), so it's the sharpest place to + // prove a crafted request with an oversized array-index field name + // (`evil[999999999]`) is rejected rather than accepted or left to hang. + it('rejects a multipart request with an oversized array-index field name', async () => { + const token = await createPublicToken(db, 'contract_action_tokens', { + contract_id: contractId, + }); + const res = await request(app) + .post(`/api/public/contracts/${token}/upload-signed-pdf`) + .field('evil[999999999]', 'x') + .attach('file', Buffer.from('%PDF-1.4 fake'), 'signed.pdf'); + // multer aborts the request before the handler runs; buildRouteApp's + // generic error handler falls back to 500 for a bare MulterError + // (see appWithErrorHandler test below for the real 4xx contract), so + // here we only assert the upload was NOT accepted/processed. + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.body.error).not.toBe(undefined); + }); + + it('maps the oversized array-index rejection to a 400 through the real error handler', async () => { + const token = await createPublicToken(db, 'contract_action_tokens', { + contract_id: contractId, + }); + const res = await request(appWithErrorHandler) + .post(`/api/public/contracts/${token}/upload-signed-pdf`) + .field('evil[999999999]', 'x') + .attach('file', Buffer.from('%PDF-1.4 fake'), 'signed.pdf'); + expect(res.status).toBe(400); + expect(res.body.code).toBe('VALIDATION_ERROR'); + }); }); describe('GET /:token/pdf', () => { diff --git a/backend/package-lock.json b/backend/package-lock.json index 81a66699..7beea481 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "picpeak-backend", - "version": "3.46.8", + "version": "3.46.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "picpeak-backend", - "version": "3.46.8", + "version": "3.46.11", "dependencies": { "@aws-sdk/client-s3": "^3.850.0", "@aws-sdk/lib-storage": "^3.850.0", @@ -37,7 +37,7 @@ "knex": "^2.4.2", "mailparser": "^3.9.9", "mime-types": "^3.0.1", - "multer": "2.2.0", + "multer": "2.3.0", "node-cron": "^3.0.2", "node-stream-zip": "^1.15.0", "nodemailer": "^9.0.1", @@ -50,7 +50,7 @@ "qrcode": "^1.5.4", "react-i18next": "^15.6.0", "sanitize-html": "2.17.7", - "sharp": "0.35.3", + "sharp": "0.35.4", "sqlite3": "^5.1.6", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", @@ -1689,9 +1689,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", - "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -1707,13 +1707,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.2" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", - "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -1729,20 +1729,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.2" + "@img/sharp-libvips-darwin-x64": "1.3.3" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", - "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.3" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -1752,9 +1752,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -1768,9 +1768,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", - "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -1784,9 +1784,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", - "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], @@ -1800,9 +1800,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", - "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], @@ -1816,9 +1816,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", - "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], @@ -1832,9 +1832,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", - "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], @@ -1848,9 +1848,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", - "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], @@ -1864,9 +1864,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", - "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], @@ -1880,9 +1880,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", - "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], @@ -1896,9 +1896,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", - "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], @@ -1912,9 +1912,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", - "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], @@ -1930,13 +1930,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.2" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", - "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], @@ -1952,13 +1952,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.2" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", - "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], @@ -1974,13 +1974,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.2" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", - "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], @@ -1996,13 +1996,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.2" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", - "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], @@ -2018,13 +2018,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.2" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", - "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], @@ -2040,13 +2040,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.2" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", - "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], @@ -2062,13 +2062,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", - "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], @@ -2084,17 +2084,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", - "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.11.1" + "@emnapi/runtime": "^1.11.3" }, "engines": { "node": ">=20.9.0" @@ -2104,16 +2104,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", - "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.3" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -2123,9 +2123,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", - "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -2142,9 +2142,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", - "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -2161,9 +2161,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", - "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -7964,9 +7964,9 @@ } }, "node_modules/joi": { - "version": "17.13.4", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", - "integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==", + "version": "17.13.7", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.7.tgz", + "integrity": "sha512-MF80Dm5Y2veNy8QWVx9Bj3ui4mo7+VPSPsR1M+oaHXV0Gx6zGX9a2F+OZG3Blby9tOlzU9Rs5FUimlEhbKtfnQ==", "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.3.0", @@ -7991,9 +7991,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", @@ -9067,9 +9067,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", - "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz", + "integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -9270,9 +9270,9 @@ } }, "node_modules/nodemailer": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", - "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.1.1.tgz", + "integrity": "sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -11061,9 +11061,9 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", - "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", @@ -11077,31 +11077,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.3", - "@img/sharp-darwin-x64": "0.35.3", - "@img/sharp-freebsd-wasm32": "0.35.3", - "@img/sharp-libvips-darwin-arm64": "1.3.2", - "@img/sharp-libvips-darwin-x64": "1.3.2", - "@img/sharp-libvips-linux-arm": "1.3.2", - "@img/sharp-libvips-linux-arm64": "1.3.2", - "@img/sharp-libvips-linux-ppc64": "1.3.2", - "@img/sharp-libvips-linux-riscv64": "1.3.2", - "@img/sharp-libvips-linux-s390x": "1.3.2", - "@img/sharp-libvips-linux-x64": "1.3.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", - "@img/sharp-libvips-linuxmusl-x64": "1.3.2", - "@img/sharp-linux-arm": "0.35.3", - "@img/sharp-linux-arm64": "0.35.3", - "@img/sharp-linux-ppc64": "0.35.3", - "@img/sharp-linux-riscv64": "0.35.3", - "@img/sharp-linux-s390x": "0.35.3", - "@img/sharp-linux-x64": "0.35.3", - "@img/sharp-linuxmusl-arm64": "0.35.3", - "@img/sharp-linuxmusl-x64": "0.35.3", - "@img/sharp-webcontainers-wasm32": "0.35.3", - "@img/sharp-win32-arm64": "0.35.3", - "@img/sharp-win32-ia32": "0.35.3", - "@img/sharp-win32-x64": "0.35.3" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" }, "peerDependenciesMeta": { "@types/node": { diff --git a/backend/package.json b/backend/package.json index f3ad17c5..d8cf2f01 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,7 +46,7 @@ "knex": "^2.4.2", "mailparser": "^3.9.9", "mime-types": "^3.0.1", - "multer": "2.2.0", + "multer": "2.3.0", "node-cron": "^3.0.2", "node-stream-zip": "^1.15.0", "nodemailer": "^9.0.1", @@ -59,7 +59,7 @@ "qrcode": "^1.5.4", "react-i18next": "^15.6.0", "sanitize-html": "2.17.7", - "sharp": "0.35.3", + "sharp": "0.35.4", "sqlite3": "^5.1.6", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", diff --git a/backend/src/config/multerConfig.js b/backend/src/config/multerConfig.js index 48f2aa69..85b31e69 100644 --- a/backend/src/config/multerConfig.js +++ b/backend/src/config/multerConfig.js @@ -120,7 +120,14 @@ const createPhotoUploader = (options = {}) => { files: options.maxFiles || 2000, fieldSize: 10 * 1024 * 1024, parts: 10000, - headerPairs: 2000 + headerPairs: 2000, + // CVE-2026-82333: no preset in this factory is currently wired up to + // a route (nothing imports createPhotoUploader et al. — routes build + // their own multer instances directly), but every preset gets the + // limit anyway so it can't be adopted later without it. None of the + // uploaders this factory builds have a legitimate use for + // array-indexed field names. + fieldArrayIndexLimit: 0 }, fileFilter: createFileFilter(ALLOWED_TYPES.media, { validateMagicNumbers: true @@ -146,7 +153,8 @@ const createLogoUploader = (options = {}) => { } }), limits: { - fileSize: options.maxSize || SIZE_LIMITS.medium + fileSize: options.maxSize || SIZE_LIMITS.medium, + fieldArrayIndexLimit: 0 // CVE-2026-82333 — see createPhotoUploader comment }, fileFilter: createFileFilter(ALLOWED_TYPES.logos, { skipMagicValidation: ['image/svg+xml'] @@ -172,7 +180,8 @@ const createFaviconUploader = (options = {}) => { } }), limits: { - fileSize: options.maxSize || SIZE_LIMITS.small + fileSize: options.maxSize || SIZE_LIMITS.small, + fieldArrayIndexLimit: 0 // CVE-2026-82333 — see createPhotoUploader comment }, fileFilter: createFileFilter(ALLOWED_TYPES.favicons, { skipMagicValidation: ['image/x-icon', 'image/vnd.microsoft.icon'] @@ -194,7 +203,8 @@ const createGalleryUploader = (destDir, options = {}) => { dest: destDir, limits: { fileSize: options.maxSize || SIZE_LIMITS.large, - files: options.maxFiles || 10 + files: options.maxFiles || 10, + fieldArrayIndexLimit: 0 // CVE-2026-82333 — see createPhotoUploader comment }, fileFilter: createFileFilter(ALLOWED_TYPES.photos) }; diff --git a/backend/src/middleware/errorHandler.js b/backend/src/middleware/errorHandler.js index 782a2370..60cefd85 100644 --- a/backend/src/middleware/errorHandler.js +++ b/backend/src/middleware/errorHandler.js @@ -92,6 +92,16 @@ const handleKnownErrors = (err) => { return new ValidationError('Unexpected file field'); } + // CVE-2026-82333: multer 2.3.0's fieldArrayIndexLimit rejects multipart + // field names with an oversized bracket array index (e.g. `a[99999999]`) + // before the DoS-prone field parser runs. Without this mapping the + // resulting MulterError has no .statusCode/.status and falls through to + // a 500 here, so map it to a proper 400 like the other multer limits. + if (err.code === 'LIMIT_FIELD_ARRAY_INDEX') { + const { ValidationError } = require('../utils/errors'); + return new ValidationError('Field name array index too large'); + } + return err; }; diff --git a/backend/src/routes/adminBackup.js b/backend/src/routes/adminBackup.js index 619b1e4d..1e2846b6 100644 --- a/backend/src/routes/adminBackup.js +++ b/backend/src/routes/adminBackup.js @@ -191,7 +191,11 @@ const picpeakUpload = multer({ destination: (req, file, cb) => cb(null, os.tmpdir()), filename: (req, file, cb) => cb(null, `picpeak-upload-${Date.now()}-${crypto.randomBytes(6).toString('hex')}.picpeak`), }), - limits: { fileSize: 5 * 1024 * 1024 * 1024 }, // 5 GB — .picpeak with photos can be large + // CVE-2026-82333: this route only ever consumes a single unnamed file + // field (`backup`) — no legitimate bracket-indexed field name (e.g. + // `a[0]`) exists in its form. fieldArrayIndexLimit: 0 rejects any field + // name using array-index syntax at all, closing multer's field-parser DoS. + limits: { fileSize: 5 * 1024 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 5 GB — .picpeak with photos can be large }); // Upload + restore a .picpeak onto THIS instance. DESTRUCTIVE: full override of diff --git a/backend/src/routes/adminBusinessProfile.js b/backend/src/routes/adminBusinessProfile.js index 2c5305ba..889f35d9 100644 --- a/backend/src/routes/adminBusinessProfile.js +++ b/backend/src/routes/adminBusinessProfile.js @@ -109,7 +109,9 @@ const pdfLogoStorage = multer.diskStorage({ const pdfLogoUpload = multer({ storage: pdfLogoStorage, - limits: { fileSize: 5 * 1024 * 1024 }, + // CVE-2026-82333: single unnamed `logo` field only — no legitimate + // array-indexed field names, so reject any bracket-index field name. + limits: { fileSize: 5 * 1024 * 1024, fieldArrayIndexLimit: 0 }, fileFilter: (_req, file, cb) => { const allowed = ['image/png', 'image/jpeg', 'image/svg+xml']; if (allowed.includes(file.mimetype)) cb(null, true); diff --git a/backend/src/routes/adminCMS.js b/backend/src/routes/adminCMS.js index 0df5902b..b08d0e64 100644 --- a/backend/src/routes/adminCMS.js +++ b/backend/src/routes/adminCMS.js @@ -31,7 +31,9 @@ const pageLogoStorage = multer.diskStorage({ const pageLogoUpload = multer({ storage: pageLogoStorage, - limits: { fileSize: 5 * 1024 * 1024 }, + // CVE-2026-82333: single unnamed `logo` field only — no legitimate + // array-indexed field names, so reject any bracket-index field name. + limits: { fileSize: 5 * 1024 * 1024, fieldArrayIndexLimit: 0 }, fileFilter: (_req, file, cb) => { const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']; if (validateFileType(file.originalname, file.mimetype, allowed)) cb(null, true); diff --git a/backend/src/routes/adminContracts.js b/backend/src/routes/adminContracts.js index d6c3564e..219d602d 100644 --- a/backend/src/routes/adminContracts.js +++ b/backend/src/routes/adminContracts.js @@ -72,7 +72,9 @@ const signedPdfStorage = multer.diskStorage({ const signedPdfUpload = multer({ storage: signedPdfStorage, - limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB + // CVE-2026-82333: single unnamed `file` field only — no legitimate + // array-indexed field names, so reject any bracket-index field name. + limits: { fileSize: 10 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 10 MB fileFilter: (req, file, cb) => { const allowed = ['application/pdf']; if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true); diff --git a/backend/src/routes/adminEvents/logo.js b/backend/src/routes/adminEvents/logo.js index 2e33fee6..75cb864c 100644 --- a/backend/src/routes/adminEvents/logo.js +++ b/backend/src/routes/adminEvents/logo.js @@ -30,7 +30,9 @@ const eventLogoStorage = multer.diskStorage({ const eventLogoUpload = multer({ storage: eventLogoStorage, - limits: { fileSize: 5 * 1024 * 1024 }, // 5MB + // CVE-2026-82333: single unnamed `logo` field only — no legitimate + // array-indexed field names, so reject any bracket-index field name. + limits: { fileSize: 5 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 5MB fileFilter: (req, file, cb) => { const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']; if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) { diff --git a/backend/src/routes/adminExpenses.js b/backend/src/routes/adminExpenses.js index 80046707..876f8dd8 100644 --- a/backend/src/routes/adminExpenses.js +++ b/backend/src/routes/adminExpenses.js @@ -41,7 +41,10 @@ function diskUpload(subdir) { }, filename: (_req, file, cb) => cb(null, `${subdir.split('/').pop()}-${Date.now()}${path.extname(file.originalname) || ''}`), }), - limits: { fileSize: 15 * 1024 * 1024 }, + // CVE-2026-82333: both callers (`inboundUpload` → 'file', `proofUpload` + // → 'proof') take a single unnamed field — no legitimate array-indexed + // field names, so reject any bracket-index field name. + limits: { fileSize: 15 * 1024 * 1024, fieldArrayIndexLimit: 0 }, fileFilter: (_req, file, cb) => (ALLOWED_MIME.includes(file.mimetype) ? cb(null, true) : cb(new Error('Only PDF, JPEG or PNG files are allowed'))), }); } diff --git a/backend/src/routes/adminInvoices.js b/backend/src/routes/adminInvoices.js index cc4cfe6e..ec6dd56c 100644 --- a/backend/src/routes/adminInvoices.js +++ b/backend/src/routes/adminInvoices.js @@ -67,7 +67,9 @@ const importedInvoiceStorage = multer.diskStorage({ }); const importedInvoiceUpload = multer({ storage: importedInvoiceStorage, - limits: { fileSize: 10 * 1024 * 1024 }, + // CVE-2026-82333: single unnamed `pdf` field only — no legitimate + // array-indexed field names, so reject any bracket-index field name. + limits: { fileSize: 10 * 1024 * 1024, fieldArrayIndexLimit: 0 }, fileFilter: (_req, file, cb) => { if (file.mimetype === 'application/pdf') cb(null, true); else cb(new Error('Only PDF files are allowed for imported invoices')); diff --git a/backend/src/routes/adminPhotos.js b/backend/src/routes/adminPhotos.js index e6c18c7a..64444637 100644 --- a/backend/src/routes/adminPhotos.js +++ b/backend/src/routes/adminPhotos.js @@ -72,7 +72,12 @@ const upload = multer({ files: 2000, // Hard safety ceiling; actual limit enforced dynamically fieldSize: 10 * 1024 * 1024, // 10MB for non-file fields parts: 10000, - headerPairs: 2000 + headerPairs: 2000, + // CVE-2026-82333: files arrive as repeated `photos` parts via + // .array('photos', N) — not bracket-indexed field names like + // `photos[0]` — so no legitimate field name uses array-index syntax + // at all. Reject any that do. + fieldArrayIndexLimit: 0 }, fileFilter: (req, file, cb) => { // req.allowedMimeTypes is populated by the middleware that runs before multer diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 861ca574..67a5f432 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -50,7 +50,10 @@ const { validateFileType } = require('../utils/fileSecurityUtils'); const upload = multer({ storage, - limits: { fileSize: 5 * 1024 * 1024 }, // 5MB + // CVE-2026-82333: single unnamed field (`logo` or `watermarkLogo`) per + // route — no legitimate array-indexed field names, so reject any + // bracket-index field name. + limits: { fileSize: 5 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 5MB fileFilter: (req, file, cb) => { // Note: SVG files are excluded from magic number validation for logos const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']; @@ -78,7 +81,9 @@ const faviconStorage = multer.diskStorage({ const faviconUpload = multer({ storage: faviconStorage, - limits: { fileSize: 2 * 1024 * 1024 }, // 2MB — roomy enough for a 512×512+ square PNG + // CVE-2026-82333: single unnamed `favicon` field only — no legitimate + // array-indexed field names, so reject any bracket-index field name. + limits: { fileSize: 2 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 2MB — roomy enough for a 512×512+ square PNG fileFilter: (req, file, cb) => { const allowedMimeTypes = ['image/png', 'image/x-icon', 'image/vnd.microsoft.icon']; const name = file.originalname.toLowerCase(); diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index c9d95470..1fde4b86 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -2342,7 +2342,12 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async ( dest: tempUploadDir, limits: { fileSize: 50 * 1024 * 1024, // 50MB per file (separate concern from #613) - files: maxFilesPerUpload + files: maxFilesPerUpload, + // CVE-2026-82333: files arrive as repeated `photos` parts via + // .array(), not bracket-indexed field names like `photos[0]` — no + // legitimate field name uses array-index syntax at all. Reject any + // that do. + fieldArrayIndexLimit: 0 }, fileFilter: (req, file, cb) => { if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) { diff --git a/backend/src/routes/publicContracts.js b/backend/src/routes/publicContracts.js index 374f45c1..bc143573 100644 --- a/backend/src/routes/publicContracts.js +++ b/backend/src/routes/publicContracts.js @@ -62,7 +62,10 @@ const signedPdfStorage = multer.diskStorage({ const signedPdfUpload = multer({ storage: signedPdfStorage, - limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB + // CVE-2026-82333: single unnamed `file` field only, and this route is + // unauthenticated (token-only) — no legitimate array-indexed field + // names, so reject any bracket-index field name. + limits: { fileSize: 10 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 10 MB fileFilter: (req, file, cb) => { if (validateFileType(file.originalname, file.mimetype, ['application/pdf'])) return cb(null, true); return cb(new Error('Only PDF files are allowed')); diff --git a/backend/src/routes/v1/events.js b/backend/src/routes/v1/events.js index d562b77a..dbb6cd1d 100644 --- a/backend/src/routes/v1/events.js +++ b/backend/src/routes/v1/events.js @@ -57,7 +57,9 @@ const photoStorage = multer.diskStorage({ }); const photoUpload = multer({ storage: photoStorage, - limits: { fileSize: 100 * 1024 * 1024 }, // 100MB per file for v1 + // CVE-2026-82333: single unnamed `photo` field only — no legitimate + // array-indexed field names, so reject any bracket-index field name. + limits: { fileSize: 100 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 100MB per file for v1 fileFilter: (_req, file, cb) => { if (/^image\//.test(file.mimetype)) cb(null, true); else cb(new Error('Only image uploads are accepted on this endpoint'));