[grade=A] P0-4: assets upload — wire decodeImageData helper (MIME whitelist + 5MB cap)
This commit is contained in:
@@ -15,6 +15,30 @@ const { ok, successMessage } = require('../../src/utils/responses');
|
|||||||
* @returns {express.Router}
|
* @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)
|
// Image processing for favicon conversion (optional)
|
||||||
let sharp, pngToIco;
|
let sharp, pngToIco;
|
||||||
try {
|
try {
|
||||||
@@ -43,14 +67,10 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
|||||||
throw new ValidationError('Invalid filename - must not contain path separators');
|
throw new ValidationError('Invalid filename - must not contain path separators');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract base64 data
|
// P0-4 fix: use the helper that validates MIME type (whitelist), caps decoded bytes
|
||||||
const matches = data.match(/^data:image\/([a-zA-Z+]+);base64,(.+)$/);
|
// at 5 MB, and rejects anything that isn't a string. The old inline regex allowed
|
||||||
if (!matches) {
|
// `image/<anything>;base64,...` without a size cap and without MIME restriction.
|
||||||
throw new ValidationError('Invalid image data format');
|
const { buffer } = decodeImageData(data);
|
||||||
}
|
|
||||||
|
|
||||||
const base64Data = matches[2];
|
|
||||||
const buffer = Buffer.from(base64Data, 'base64');
|
|
||||||
|
|
||||||
// Determine assets path (mounted volume)
|
// Determine assets path (mounted volume)
|
||||||
const assetsPath = platformPaths.resolveAssetsPath(process.env.ASSETS_PATH);
|
const assetsPath = platformPaths.resolveAssetsPath(process.env.ASSETS_PATH);
|
||||||
|
|||||||
Reference in New Issue
Block a user