archetecture security fix
This commit is contained in:
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.imageUpload = void 0;
|
||||
exports.detectImageType = detectImageType;
|
||||
exports.assertImageFile = assertImageFile;
|
||||
exports.assertImageFiles = assertImageFiles;
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const multer_1 = __importDefault(require("multer"));
|
||||
const errors_1 = require("../errors");
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
const ALLOWED_IMAGE_TYPES = new Map([
|
||||
['image/jpeg', ['.jpg', '.jpeg']],
|
||||
['image/png', ['.png']],
|
||||
['image/webp', ['.webp']],
|
||||
['image/gif', ['.gif']],
|
||||
]);
|
||||
/**
|
||||
* Shared multer instance used by all upload endpoints.
|
||||
* Files are kept in memory so services receive a Buffer — no temp-file cleanup needed.
|
||||
*/
|
||||
exports.imageUpload = (0, multer_1.default)({
|
||||
storage: multer_1.default.memoryStorage(),
|
||||
limits: { fileSize: MAX_FILE_SIZE, files: 20 },
|
||||
});
|
||||
function detectImageType(buffer) {
|
||||
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
||||
return { mime: 'image/jpeg', ext: '.jpg' };
|
||||
}
|
||||
if (buffer.length >= 8 &&
|
||||
buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47 &&
|
||||
buffer[4] === 0x0d && buffer[5] === 0x0a && buffer[6] === 0x1a && buffer[7] === 0x0a) {
|
||||
return { mime: 'image/png', ext: '.png' };
|
||||
}
|
||||
if (buffer.length >= 12 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP') {
|
||||
return { mime: 'image/webp', ext: '.webp' };
|
||||
}
|
||||
if (buffer.length >= 6) {
|
||||
const sig = buffer.subarray(0, 6).toString('ascii');
|
||||
if (sig === 'GIF87a' || sig === 'GIF89a')
|
||||
return { mime: 'image/gif', ext: '.gif' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function assertSafeImageContent(file) {
|
||||
const detected = detectImageType(file.buffer);
|
||||
if (!detected || !ALLOWED_IMAGE_TYPES.has(detected.mime)) {
|
||||
throw new errors_1.ValidationError('Unsupported or spoofed image file');
|
||||
}
|
||||
if (file.mimetype !== detected.mime) {
|
||||
throw new errors_1.ValidationError(`MIME type does not match file content for "${file.originalname}"`);
|
||||
}
|
||||
const ext = path_1.default.extname(file.originalname).toLowerCase();
|
||||
const allowedExtensions = ALLOWED_IMAGE_TYPES.get(detected.mime) ?? [];
|
||||
if (ext && !allowedExtensions.includes(ext)) {
|
||||
throw new errors_1.ValidationError(`File extension does not match file content for "${file.originalname}"`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Asserts that a file was provided and is an image by content, not by client claims.
|
||||
*/
|
||||
function assertImageFile(file, fieldLabel = 'file') {
|
||||
if (!file)
|
||||
throw new errors_1.ValidationError(`A ${fieldLabel} is required`);
|
||||
assertSafeImageContent(file);
|
||||
}
|
||||
/**
|
||||
* Asserts that at least one file was provided and all files are image content.
|
||||
*/
|
||||
function assertImageFiles(files, fieldLabel = 'photos') {
|
||||
if (!files || files.length === 0)
|
||||
throw new errors_1.ValidationError(`At least one ${fieldLabel} file is required`);
|
||||
for (const file of files)
|
||||
assertSafeImageContent(file);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Reference in New Issue
Block a user