Files
workinf_Blender_Wasm/tools/web/generate-malicious-archive-fixtures.mjs
mes123456 380cbed4ff
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

189 lines
6.5 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const outputArgument = process.argv.indexOf("--output");
const outputRoot = outputArgument === -1
? path.join(repoRoot, "tests/files/web/archive-security")
: path.resolve(process.argv[outputArgument + 1] ?? "");
if (!outputRoot) throw new Error("--output requires a directory");
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const text = (value) => Buffer.from(value, "utf8");
const crcTable = Array.from({ length: 256 }, (_, input) => {
let value = input;
for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ value >>> 1 : value >>> 1;
return value >>> 0;
});
function crc32(bytes) {
let value = 0xffffffff;
for (const byte of bytes) value = crcTable[(value ^ byte) & 0xff] ^ value >>> 8;
return (value ^ 0xffffffff) >>> 0;
}
function zipArchive(entries) {
const localParts = [];
const centralParts = [];
let localOffset = 0;
for (const entry of entries) {
const name = text(entry.path);
const data = Buffer.from(entry.data);
const uncompressedBytes = entry.uncompressedBytes ?? data.length;
const crc = crc32(data);
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0);
local.writeUInt16LE(20, 4);
local.writeUInt16LE(0x0800, 6);
local.writeUInt16LE(0, 8);
local.writeUInt32LE(crc, 14);
local.writeUInt32LE(data.length, 18);
local.writeUInt32LE(uncompressedBytes, 22);
local.writeUInt16LE(name.length, 26);
localParts.push(local, name, data);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0);
central.writeUInt16LE(0x0314, 4);
central.writeUInt16LE(20, 6);
central.writeUInt16LE(0x0800, 8);
central.writeUInt16LE(0, 10);
central.writeUInt32LE(crc, 16);
central.writeUInt32LE(data.length, 20);
central.writeUInt32LE(uncompressedBytes, 24);
central.writeUInt16LE(name.length, 28);
central.writeUInt32LE((0o100644 << 16) >>> 0, 38);
central.writeUInt32LE(localOffset, 42);
centralParts.push(central, name);
localOffset += local.length + name.length + data.length;
}
const centralDirectory = Buffer.concat(centralParts);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(entries.length, 8);
end.writeUInt16LE(entries.length, 10);
end.writeUInt32LE(centralDirectory.length, 12);
end.writeUInt32LE(localOffset, 16);
return Buffer.concat([...localParts, centralDirectory, end]);
}
function writeTarText(header, value, offset, length) {
const bytes = text(value);
if (bytes.length > length) throw new Error(`tar field exceeds ${length} bytes`);
bytes.copy(header, offset);
}
function writeTarOctal(header, value, offset, length) {
const encoded = value.toString(8).padStart(length - 2, "0");
writeTarText(header, `${encoded}\0 `, offset, length);
}
function tarHeader(entry) {
const header = Buffer.alloc(512);
writeTarText(header, entry.path, 0, 100);
writeTarOctal(header, entry.type === "DIRECTORY" ? 0o755 : 0o644, 100, 8);
writeTarOctal(header, 0, 108, 8);
writeTarOctal(header, 0, 116, 8);
const data = entry.type === "FILE" ? Buffer.from(entry.data) : Buffer.alloc(0);
writeTarOctal(header, data.length, 124, 12);
writeTarOctal(header, 0, 136, 12);
header.fill(0x20, 148, 156);
header[156] = { FILE: 0x30, SYMLINK: 0x32, HARDLINK: 0x31, DIRECTORY: 0x35 }[entry.type];
if (entry.target) writeTarText(header, entry.target, 157, 100);
writeTarText(header, "ustar\0", 257, 6);
writeTarText(header, "00", 263, 2);
writeTarText(header, "root", 265, 32);
writeTarText(header, "root", 297, 32);
const checksum = header.reduce((sum, byte) => sum + byte, 0);
writeTarOctal(header, checksum, 148, 8);
return { header, data };
}
function tarArchive(entries) {
const parts = [];
for (const entry of entries) {
const { header, data } = tarHeader(entry);
parts.push(header, data);
if (data.length % 512 !== 0) parts.push(Buffer.alloc(512 - data.length % 512));
}
parts.push(Buffer.alloc(1024));
return Buffer.concat(parts);
}
const definitions = [
{
id: "ZIP_PATH_TRAVERSAL",
format: "ZIP",
file: "zip-path-traversal.zip",
threat: "ARCHIVE_ROOT_ESCAPE",
gate: "LINK_SAFETY",
bytes: zipArchive([{ path: "../outside.txt", data: text("escape") }]),
},
{
id: "ZIP_COMPRESSION_BOMB",
format: "ZIP",
file: "zip-compression-bomb.zip",
threat: "COMPRESSION_RATIO",
gate: "CONFLICTS",
bytes: zipArchive([{ path: "bomb.bin", data: Buffer.from([0]), uncompressedBytes: 101 }]),
},
{
id: "ZIP_DUPLICATE_PATH",
format: "ZIP",
file: "zip-duplicate-path.zip",
threat: "DUPLICATE_PATH",
gate: "LINK_SAFETY",
bytes: zipArchive([{ path: "same.bin", data: text("one") }, { path: "same.bin", data: text("two") }]),
},
{
id: "TAR_PATH_TRAVERSAL",
format: "TAR",
file: "tar-path-traversal.tar",
threat: "ARCHIVE_ROOT_ESCAPE",
gate: "LINK_SAFETY",
bytes: tarArchive([{ path: "../../outside.txt", type: "FILE", data: text("escape") }]),
},
{
id: "TAR_SYMLINK_ESCAPE",
format: "TAR",
file: "tar-symlink-escape.tar",
threat: "SYMLINK_ESCAPE",
gate: "LINK_SAFETY",
bytes: tarArchive([
{ path: "safe", type: "DIRECTORY" },
{ path: "safe/link", type: "SYMLINK", target: "../../outside" },
]),
},
{
id: "TAR_PREFIX_CONFLICT",
format: "TAR",
file: "tar-prefix-conflict.tar",
threat: "FILE_DIRECTORY_PREFIX_CONFLICT",
gate: "CONFLICTS",
bytes: tarArchive([
{ path: "folder", type: "FILE", data: text("one") },
{ path: "folder/payload.bin", type: "FILE", data: text("two") },
]),
},
];
fs.mkdirSync(outputRoot, { recursive: true });
for (const definition of definitions) fs.writeFileSync(path.join(outputRoot, definition.file), definition.bytes);
const manifest = {
schemaVersion: 1,
task: "M12-04J",
generator: "tools/web/generate-malicious-archive-fixtures.mjs",
extractionAllowed: false,
cases: definitions.map(({ bytes, ...definition }) => ({
...definition,
byteLength: bytes.length,
sha256: sha256(bytes),
expectedCode: "IO_ARCHIVE_UNSAFE",
})),
};
fs.writeFileSync(path.join(outputRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
process.stdout.write(`malicious-archive-fixtures-generated cases=${definitions.length} output=${outputRoot}\n`);