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.
This commit is contained in:
@@ -25,14 +25,18 @@ process.env.STORAGE_PATH = path.join(tmpDir, 'storage');
|
|||||||
fs.mkdirSync(process.env.STORAGE_PATH, { recursive: true });
|
fs.mkdirSync(process.env.STORAGE_PATH, { recursive: true });
|
||||||
process.env.JWT_SECRET = process.env.JWT_SECRET || 'crm-route-test-secret';
|
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 request = require('supertest');
|
||||||
const { bootCrmDb, seedMinimal, createPublicToken, buildRouteApp } = require('../integration/helpers/crmDb');
|
const { bootCrmDb, seedMinimal, createPublicToken, buildRouteApp } = require('../integration/helpers/crmDb');
|
||||||
const tokenGuards = require('../../src/utils/publicTokenGuards');
|
const tokenGuards = require('../../src/utils/publicTokenGuards');
|
||||||
|
const { errorHandler } = require('../../src/middleware/errorHandler');
|
||||||
|
|
||||||
describe('publicContracts routes', () => {
|
describe('publicContracts routes', () => {
|
||||||
let db;
|
let db;
|
||||||
let cleanup;
|
let cleanup;
|
||||||
let app;
|
let app;
|
||||||
|
let appWithErrorHandler;
|
||||||
let customerId;
|
let customerId;
|
||||||
let contractId;
|
let contractId;
|
||||||
|
|
||||||
@@ -51,6 +55,17 @@ describe('publicContracts routes', () => {
|
|||||||
contractId = inserted[0]?.id ?? inserted[0];
|
contractId = inserted[0]?.id ?? inserted[0];
|
||||||
|
|
||||||
app = buildRouteApp('/api/public/contracts', require('../../src/routes/publicContracts'));
|
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);
|
}, 120000);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -131,6 +146,40 @@ describe('publicContracts routes', () => {
|
|||||||
.attach('file', Buffer.from('%PDF-1.4 fake'), 'signed.pdf');
|
.attach('file', Buffer.from('%PDF-1.4 fake'), 'signed.pdf');
|
||||||
expect(res.status).toBe(404);
|
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', () => {
|
describe('GET /:token/pdf', () => {
|
||||||
|
|||||||
@@ -120,7 +120,14 @@ const createPhotoUploader = (options = {}) => {
|
|||||||
files: options.maxFiles || 2000,
|
files: options.maxFiles || 2000,
|
||||||
fieldSize: 10 * 1024 * 1024,
|
fieldSize: 10 * 1024 * 1024,
|
||||||
parts: 10000,
|
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, {
|
fileFilter: createFileFilter(ALLOWED_TYPES.media, {
|
||||||
validateMagicNumbers: true
|
validateMagicNumbers: true
|
||||||
@@ -146,7 +153,8 @@ const createLogoUploader = (options = {}) => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
limits: {
|
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, {
|
fileFilter: createFileFilter(ALLOWED_TYPES.logos, {
|
||||||
skipMagicValidation: ['image/svg+xml']
|
skipMagicValidation: ['image/svg+xml']
|
||||||
@@ -172,7 +180,8 @@ const createFaviconUploader = (options = {}) => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
limits: {
|
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, {
|
fileFilter: createFileFilter(ALLOWED_TYPES.favicons, {
|
||||||
skipMagicValidation: ['image/x-icon', 'image/vnd.microsoft.icon']
|
skipMagicValidation: ['image/x-icon', 'image/vnd.microsoft.icon']
|
||||||
@@ -194,7 +203,8 @@ const createGalleryUploader = (destDir, options = {}) => {
|
|||||||
dest: destDir,
|
dest: destDir,
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: options.maxSize || SIZE_LIMITS.large,
|
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)
|
fileFilter: createFileFilter(ALLOWED_TYPES.photos)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ const handleKnownErrors = (err) => {
|
|||||||
return new ValidationError('Unexpected file field');
|
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;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -202,7 +202,11 @@ const picpeakUpload = multer({
|
|||||||
destination: (req, file, cb) => cb(null, os.tmpdir()),
|
destination: (req, file, cb) => cb(null, os.tmpdir()),
|
||||||
filename: (req, file, cb) => cb(null, `picpeak-upload-${Date.now()}-${crypto.randomBytes(6).toString('hex')}.picpeak`),
|
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
|
// Upload + restore a .picpeak onto THIS instance. DESTRUCTIVE: full override of
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ const pdfLogoStorage = multer.diskStorage({
|
|||||||
|
|
||||||
const pdfLogoUpload = multer({
|
const pdfLogoUpload = multer({
|
||||||
storage: pdfLogoStorage,
|
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) => {
|
fileFilter: (_req, file, cb) => {
|
||||||
const allowed = ['image/png', 'image/jpeg', 'image/svg+xml'];
|
const allowed = ['image/png', 'image/jpeg', 'image/svg+xml'];
|
||||||
if (allowed.includes(file.mimetype)) cb(null, true);
|
if (allowed.includes(file.mimetype)) cb(null, true);
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ const pageLogoStorage = multer.diskStorage({
|
|||||||
|
|
||||||
const pageLogoUpload = multer({
|
const pageLogoUpload = multer({
|
||||||
storage: pageLogoStorage,
|
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) => {
|
fileFilter: (_req, file, cb) => {
|
||||||
const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
|
const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
|
||||||
if (validateFileType(file.originalname, file.mimetype, allowed)) cb(null, true);
|
if (validateFileType(file.originalname, file.mimetype, allowed)) cb(null, true);
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ const signedPdfStorage = multer.diskStorage({
|
|||||||
|
|
||||||
const signedPdfUpload = multer({
|
const signedPdfUpload = multer({
|
||||||
storage: signedPdfStorage,
|
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) => {
|
fileFilter: (req, file, cb) => {
|
||||||
const allowed = ['application/pdf'];
|
const allowed = ['application/pdf'];
|
||||||
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
|
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ const eventLogoStorage = multer.diskStorage({
|
|||||||
|
|
||||||
const eventLogoUpload = multer({
|
const eventLogoUpload = multer({
|
||||||
storage: eventLogoStorage,
|
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) => {
|
fileFilter: (req, file, cb) => {
|
||||||
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
|
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
|
||||||
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
|
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ function diskUpload(subdir) {
|
|||||||
},
|
},
|
||||||
filename: (_req, file, cb) => cb(null, `${subdir.split('/').pop()}-${Date.now()}${path.extname(file.originalname) || ''}`),
|
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'))),
|
fileFilter: (_req, file, cb) => (ALLOWED_MIME.includes(file.mimetype) ? cb(null, true) : cb(new Error('Only PDF, JPEG or PNG files are allowed'))),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ const importedInvoiceStorage = multer.diskStorage({
|
|||||||
});
|
});
|
||||||
const importedInvoiceUpload = multer({
|
const importedInvoiceUpload = multer({
|
||||||
storage: importedInvoiceStorage,
|
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) => {
|
fileFilter: (_req, file, cb) => {
|
||||||
if (file.mimetype === 'application/pdf') cb(null, true);
|
if (file.mimetype === 'application/pdf') cb(null, true);
|
||||||
else cb(new Error('Only PDF files are allowed for imported invoices'));
|
else cb(new Error('Only PDF files are allowed for imported invoices'));
|
||||||
|
|||||||
@@ -112,7 +112,12 @@ const createUpload = (maxFileSizeBytes) => multer({
|
|||||||
files: 2000, // Hard safety ceiling; actual limit enforced dynamically
|
files: 2000, // Hard safety ceiling; actual limit enforced dynamically
|
||||||
fieldSize: 10 * 1024 * 1024, // 10MB for non-file fields
|
fieldSize: 10 * 1024 * 1024, // 10MB for non-file fields
|
||||||
parts: 10000,
|
parts: 10000,
|
||||||
headerPairs: 2000
|
headerPairs: 2000,
|
||||||
|
// CVE-2026-82333: files arrive as repeated `photos` parts via
|
||||||
|
// multer's own .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) => {
|
fileFilter: (req, file, cb) => {
|
||||||
// req.allowedMimeTypes is populated by the middleware that runs before multer
|
// req.allowedMimeTypes is populated by the middleware that runs before multer
|
||||||
|
|||||||
@@ -153,7 +153,10 @@ const { validateFileType } = require('../utils/fileSecurityUtils');
|
|||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage,
|
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) => {
|
fileFilter: (req, file, cb) => {
|
||||||
// Note: SVG files are excluded from magic number validation for logos
|
// Note: SVG files are excluded from magic number validation for logos
|
||||||
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
|
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
|
||||||
@@ -181,7 +184,9 @@ const faviconStorage = multer.diskStorage({
|
|||||||
|
|
||||||
const faviconUpload = multer({
|
const faviconUpload = multer({
|
||||||
storage: faviconStorage,
|
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) => {
|
fileFilter: (req, file, cb) => {
|
||||||
const allowedMimeTypes = ['image/png', 'image/x-icon', 'image/vnd.microsoft.icon'];
|
const allowedMimeTypes = ['image/png', 'image/x-icon', 'image/vnd.microsoft.icon'];
|
||||||
const name = file.originalname.toLowerCase();
|
const name = file.originalname.toLowerCase();
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ const tempStorage = multer.diskStorage({
|
|||||||
function buildAdminUploader(maxSizeBytes, allowed) {
|
function buildAdminUploader(maxSizeBytes, allowed) {
|
||||||
return multer({
|
return multer({
|
||||||
storage: tempStorage,
|
storage: tempStorage,
|
||||||
limits: { fileSize: maxSizeBytes, files: ADMIN_MAX_FILES },
|
// CVE-2026-82333: files arrive as repeated `files` parts via .array(),
|
||||||
|
// not bracket-indexed field names like `files[0]` — no legitimate
|
||||||
|
// field name uses array-index syntax at all. Reject any that do.
|
||||||
|
limits: { fileSize: maxSizeBytes, files: ADMIN_MAX_FILES, fieldArrayIndexLimit: 0 },
|
||||||
fileFilter: (req, file, cb) => {
|
fileFilter: (req, file, cb) => {
|
||||||
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
|
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
|
||||||
return cb(new Error('This file type is not allowed'));
|
return cb(new Error('This file type is not allowed'));
|
||||||
|
|||||||
@@ -77,7 +77,12 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
|
|||||||
dest: tempUploadDir,
|
dest: tempUploadDir,
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: maxFileSizeBytes,
|
fileSize: maxFileSizeBytes,
|
||||||
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) => {
|
fileFilter: (req, file, cb) => {
|
||||||
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
|
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ const signedPdfStorage = multer.diskStorage({
|
|||||||
|
|
||||||
const signedPdfUpload = multer({
|
const signedPdfUpload = multer({
|
||||||
storage: signedPdfStorage,
|
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) => {
|
fileFilter: (req, file, cb) => {
|
||||||
if (validateFileType(file.originalname, file.mimetype, ['application/pdf'])) return cb(null, true);
|
if (validateFileType(file.originalname, file.mimetype, ['application/pdf'])) return cb(null, true);
|
||||||
return cb(new Error('Only PDF files are allowed'));
|
return cb(new Error('Only PDF files are allowed'));
|
||||||
|
|||||||
@@ -122,7 +122,11 @@ const tempStorage = multer.diskStorage({
|
|||||||
function buildUploader(maxSizeBytes, allowed) {
|
function buildUploader(maxSizeBytes, allowed) {
|
||||||
return multer({
|
return multer({
|
||||||
storage: tempStorage,
|
storage: tempStorage,
|
||||||
limits: { fileSize: maxSizeBytes, files: MAX_FILES_PER_UPLOAD },
|
// CVE-2026-82333: files arrive as repeated `files` parts via .array(),
|
||||||
|
// not bracket-indexed field names like `files[0]`, and this route is
|
||||||
|
// unauthenticated (token-only) — no legitimate field name uses
|
||||||
|
// array-index syntax at all. Reject any that do.
|
||||||
|
limits: { fileSize: maxSizeBytes, files: MAX_FILES_PER_UPLOAD, fieldArrayIndexLimit: 0 },
|
||||||
fileFilter: (req, file, cb) => {
|
fileFilter: (req, file, cb) => {
|
||||||
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
|
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
|
||||||
return cb(new Error('This file type is not allowed'));
|
return cb(new Error('This file type is not allowed'));
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ const photoStorage = multer.diskStorage({
|
|||||||
});
|
});
|
||||||
const buildPhotoUpload = (maxFileSizeBytes) => multer({
|
const buildPhotoUpload = (maxFileSizeBytes) => multer({
|
||||||
storage: photoStorage,
|
storage: photoStorage,
|
||||||
limits: { fileSize: maxFileSizeBytes },
|
// CVE-2026-82333: single unnamed `photo` field only — no legitimate
|
||||||
|
// array-indexed field names, so reject any bracket-index field name.
|
||||||
|
limits: { fileSize: maxFileSizeBytes, fieldArrayIndexLimit: 0 },
|
||||||
fileFilter: (_req, file, cb) => {
|
fileFilter: (_req, file, cb) => {
|
||||||
if (/^image\//.test(file.mimetype)) cb(null, true);
|
if (/^image\//.test(file.mimetype)) cb(null, true);
|
||||||
else cb(new Error('Only image uploads are accepted on this endpoint'));
|
else cb(new Error('Only image uploads are accepted on this endpoint'));
|
||||||
|
|||||||
Reference in New Issue
Block a user