65 lines
3.4 KiB
JavaScript
65 lines
3.4 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
|
|
const {
|
|
cleanupUploadedFiles,
|
|
safeDownloadFilename,
|
|
validateUploadedFile,
|
|
validateUploadedFiles
|
|
} = require("../src/services/upload-security");
|
|
|
|
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-upload-security-"));
|
|
|
|
function upload(name, mime, bytes) {
|
|
const filePath = path.join(sandbox, `${Date.now()}-${Math.random()}`);
|
|
fs.writeFileSync(filePath, bytes);
|
|
return { path: filePath, originalname: name, mimetype: mime, size: bytes.length };
|
|
}
|
|
|
|
try {
|
|
const png = upload("icon.png", "image/png", Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00]));
|
|
validateUploadedFile(png, { allowedTypes: ["png"], maxBytes: 1024, requireExtension: true });
|
|
assert.equal(png.detectedType, "png");
|
|
|
|
const disguised = upload("not-really.png", "image/png", Buffer.from("not an image"));
|
|
assert.throws(() => validateUploadedFiles([disguised], { allowedTypes: ["png"], maxBytes: 1024 }), /contents/);
|
|
assert.equal(fs.existsSync(disguised.path), false);
|
|
|
|
const mismatch = upload("photo.jpg", "image/jpeg", Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
|
assert.throws(() => validateUploadedFiles([mismatch], { allowedTypes: ["png", "jpeg"] }), /browser|extension/);
|
|
assert.equal(fs.existsSync(mismatch.path), false);
|
|
|
|
const unsafeSvg = upload("icon.svg", "image/svg+xml", Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>'));
|
|
assert.throws(() => validateUploadedFiles([unsafeSvg], { allowedTypes: ["svg"] }), /contents/);
|
|
assert.equal(fs.existsSync(unsafeSvg.path), false);
|
|
|
|
const safeSvg = upload("icon.svg", "image/svg+xml", Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"><path d="M0 0h1v1z"/></svg>'));
|
|
validateUploadedFile(safeSvg, { allowedTypes: ["svg"], requireExtension: true });
|
|
|
|
const oversized = upload("large.txt", "text/plain", Buffer.from("12345"));
|
|
assert.throws(() => validateUploadedFiles([oversized], { allowedTypes: ["text"], maxBytes: 4 }), /larger/);
|
|
assert.equal(fs.existsSync(oversized.path), false);
|
|
|
|
assert.equal(safeDownloadFilename('../../bad\r\n"name.txt'), "badname.txt");
|
|
assert.equal(safeDownloadFilename("..\\..\\secret.txt"), "secret.txt");
|
|
|
|
const serverSource = fs.readFileSync(path.join(__dirname, "..", "src", "web", "server.js"), "utf8");
|
|
const moderationSource = fs.readFileSync(path.join(__dirname, "..", "plugins", "moderation", "index.js"), "utf8");
|
|
const economySource = fs.readFileSync(path.join(__dirname, "..", "plugins", "economy-framework", "index.js"), "utf8");
|
|
assert(serverSource.includes('allowedTypes: ["zip"]'));
|
|
assert(serverSource.includes('allowedTypes: ["png", "svg"]'));
|
|
assert(serverSource.includes('safeDownloadFilename(attachment.original_name'));
|
|
assert(serverSource.includes('"X-Content-Type-Options", "nosniff"'));
|
|
assert(moderationSource.includes("validateUploadedFiles(req.files"));
|
|
assert(moderationSource.indexOf('router.post("/actions", (req, res, next)') < moderationSource.indexOf("evidenceUpload, async"));
|
|
assert(economySource.includes("currencyIconUpload"));
|
|
assert(economySource.indexOf('router.post("/settings/icon", (req, res, next)') < economySource.indexOf("currencyIconUpload, (req, res)"));
|
|
|
|
cleanupUploadedFiles([png, safeSvg]);
|
|
console.log("Upload security verification passed.");
|
|
} finally {
|
|
fs.rmSync(sandbox, { recursive: true, force: true });
|
|
}
|