From 57ed09fe9139bc0312809302ab39083d03707492 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 8 Aug 2026 03:33:32 -0700 Subject: [PATCH] =?UTF-8?q?[grade=3DA]=20P0-4:=20assets=20upload=20?= =?UTF-8?q?=E2=80=94=20wire=20decodeImageData=20helper=20(MIME=20whitelist?= =?UTF-8?q?=20+=205MB=20cap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashcaddy-api/routes/config/assets.js | 36 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/dashcaddy-api/routes/config/assets.js b/dashcaddy-api/routes/config/assets.js index 6aedb0f..f695e0c 100644 --- a/dashcaddy-api/routes/config/assets.js +++ b/dashcaddy-api/routes/config/assets.js @@ -15,6 +15,30 @@ const { ok, successMessage } = require('../../src/utils/responses'); * @returns {express.Router} */ +// P0-4: image upload guard rails. +// We allow a small whitelist of image MIME types and cap decoded bytes at 5 MB. +const ALLOWED_IMAGE_TYPES = new Set(['png', 'jpeg', 'jpg', 'svg+xml', 'webp', 'ico', 'x-icon']); +const MAX_ASSET_BYTES = 5 * 1024 * 1024; + +function decodeImageData(data) { + if (typeof data !== 'string') { + throw new ValidationError('Invalid image data format'); + } + const matches = data.match(/^data:image\/([a-zA-Z0-9+.-]+);base64,(.+)$/); + if (!matches) { + throw new ValidationError('Invalid image data format'); + } + const mime = matches[1].toLowerCase(); + if (!ALLOWED_IMAGE_TYPES.has(mime)) { + throw new ValidationError(`Unsupported image type: ${mime}`); + } + const buffer = Buffer.from(matches[2], 'base64'); + if (buffer.length > MAX_ASSET_BYTES) { + throw new ValidationError(`File too large (max ${MAX_ASSET_BYTES / 1024 / 1024}MB)`); + } + return { mime, buffer }; +} + // Image processing for favicon conversion (optional) let sharp, pngToIco; try { @@ -43,14 +67,10 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa throw new ValidationError('Invalid filename - must not contain path separators'); } - // Extract base64 data - const matches = data.match(/^data:image\/([a-zA-Z+]+);base64,(.+)$/); - if (!matches) { - throw new ValidationError('Invalid image data format'); - } - - const base64Data = matches[2]; - const buffer = Buffer.from(base64Data, 'base64'); + // P0-4 fix: use the helper that validates MIME type (whitelist), caps decoded bytes + // at 5 MB, and rejects anything that isn't a string. The old inline regex allowed + // `image/;base64,...` without a size cap and without MIME restriction. + const { buffer } = decodeImageData(data); // Determine assets path (mounted volume) const assetsPath = platformPaths.resolveAssetsPath(process.env.ASSETS_PATH);