diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs index 20c9584823..23b2051b1a 100644 --- a/demo/web/archive.mjs +++ b/demo/web/archive.mjs @@ -18,7 +18,7 @@ class BoundedBlobReader extends BlobReader { export function validPath(name) { if (!name || name.includes("\\") || /[\x00-\x1f<>:"|?*]/u.test(name)) throw new Error(`Unsafe ZIP path: ${name}`); const parts = name.replace(/\/$/u, "").split("/"); - if (parts.some(part => !part || part === "." || part === ".." || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(part))) throw new Error(`Unsafe ZIP path: ${name}`); + if (parts.some(part => !part || part === "." || part === ".." || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9\u00b9\u00b2\u00b3]|lpt[1-9\u00b9\u00b2\u00b3])(?:\.|$)/iu.test(part))) throw new Error(`Unsafe ZIP path: ${name}`); return name; } diff --git a/demo/web/tests/archive-reserved.test.mjs b/demo/web/tests/archive-reserved.test.mjs new file mode 100644 index 0000000000..b04432d014 --- /dev/null +++ b/demo/web/tests/archive-reserved.test.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; + +register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); +const { Archive, validPath } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); +const { BlobWriter, Uint8ArrayReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +const header = new Uint8Array([127, 69, 76, 70]); + +for (const prefix of ["COM", "lpt"]) { + for (const digit of ["\u00b9", "\u00b2", "\u00b3"]) { + for (const name of [prefix + digit, prefix + digit + ".txt", "content/" + prefix + digit + "/data.bin"]) { + assert.throws(() => validPath(name), /Unsafe ZIP path/); + const folder = new Archive([{ path: "main.elf", file: new Blob([header]) }, { path: name, file: new Blob(["resource"]) }]); + try { await assert.rejects(folder.inspect(() => {}), /Unsafe ZIP path/); } + finally { await folder.close(); } + } + } +} + +const writer = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); +await writer.add("main.elf", new Uint8ArrayReader(header)); +await writer.add("content/COM\u00b9.txt", new Uint8ArrayReader(new Uint8Array([1]))); +const archive = new Archive(await writer.close()); +try { await assert.rejects(archive.inspect(() => {}), /Unsafe ZIP path/); } +finally { await archive.close(); } + +for (const name of ["COM10", "COM\u2074.txt", "COM\u00b9-extra", "content/ordinary.txt"]) { + assert.equal(validPath(name), name); +} diff --git a/demo/web/tests/archive.test.mjs b/demo/web/tests/archive.test.mjs index 4470741f74..2ad5bce8eb 100644 --- a/demo/web/tests/archive.test.mjs +++ b/demo/web/tests/archive.test.mjs @@ -9,6 +9,7 @@ register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_ const site = name => pathToFileURL(path.join(process.argv[2], name)).href; const { Archive, memoryEstimate, MEMORY_LIMIT, packageArchive } = await import(site("archive.mjs")); const { BlobReader, BlobWriter, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +await import("./archive-reserved.test.mjs"); const { default: createRelinker } = await import(site("native/relinker_wasm.mjs")); const native = await createRelinker(); const executable = new Uint8Array(await readFile(path.join(process.argv[3], "main.elf")));