diff --git a/mod.ts b/mod.ts index 19ce118..9f6855f 100644 --- a/mod.ts +++ b/mod.ts @@ -1,6 +1,6 @@ -import _JSZip from "https://dev.jspm.io/jszip@3.5.0"; +import _JSZip from "https://dev.jspm.io/jszip@3.5.0"; import { WalkOptions, walk } from "https://deno.land/std@0.99.0/fs/walk.ts"; -import { SEP, resolve, dirname } from "https://deno.land/std@0.99.0/path/mod.ts"; +import { SEP, resolve, dirname, globToRegExp } from "https://deno.land/std@0.99.0/path/mod.ts"; import type { InputFileFormat, JSZipAddFileOptions, @@ -109,7 +109,7 @@ export class JSZip { content?: string | Uint8Array, options?: JSZipAddFileOptions, ): JSZipObject { - const replaceBackslashes = options?.replaceBackslashes === undefined || options.replaceBackslashes; + const replaceBackslashes = options?.replaceBackslashes === undefined || options.replaceBackslashes; const finalPath = replaceBackslashes ? path.replaceAll("\\", "/") : path; // @ts-ignores @@ -186,7 +186,7 @@ export class JSZip { const b: Uint8Array = await this.generateAsync({ type: "uint8array" }); return await Deno.writeFile(path, b); } - + /** * Unzip a JSZip asynchronously to a directory * @@ -196,22 +196,26 @@ export class JSZip { async unzip(dir: string = "."): Promise { // FIXME optionally replace the existing folder prefix with dir. const createdDirs = new Set(); + const allowedFileLocRegex = globToRegExp(resolve(dir, "**")); for (const fileEntry of this) { - const filePath = resolve(dir, fileEntry.name); + const filePath = resolve(dir, fileEntry.name); + if (!allowedFileLocRegex.test(filePath)) { + throw new Error("Not allowed!"); + } - const dirPath = fileEntry.dir ? filePath : dirname(filePath); + const dirPath = fileEntry.dir ? filePath : dirname(filePath); - if (!createdDirs.has(dirPath)) { - await Deno.mkdir(dirPath, { recursive: true }); - createdDirs.add(dirPath); - } + if (!createdDirs.has(dirPath)) { + await Deno.mkdir(dirPath, { recursive: true }); + createdDirs.add(dirPath); + } - if (!fileEntry.dir) { - const content = await fileEntry.async("uint8array"); - // TODO pass WriteFileOptions e.g. mode - await Deno.writeFile(filePath, content); - } + if (!fileEntry.dir) { + const content = await fileEntry.async("uint8array"); + // TODO pass WriteFileOptions e.g. mode + await Deno.writeFile(filePath, content); + } } } diff --git a/test.ts b/test.ts index 2d8c835..794deb9 100644 --- a/test.ts +++ b/test.ts @@ -1,7 +1,8 @@ import { decode, encode } from "https://deno.land/std@0.74.0/encoding/utf8.ts"; import { join } from "https://deno.land/std@0.74.0/path/mod.ts"; -import { assertEquals } from "https://deno.land/std@0.74.0/testing/asserts.ts"; +import { assert, assertEquals, assertThrowsAsync } from "https://deno.land/std@0.74.0/testing/asserts.ts"; import { JSZip, readZip, zipDir } from "./mod.ts"; +import { exists } from "https://deno.land/std@0.100.0/fs/mod.ts"; // FIXME use tmp directory and clean up. async function exampleZip(path: string, createDirectories = true) { @@ -20,6 +21,19 @@ async function exampleZip(path: string, createDirectories = true) { await zip.writeZip(path); } +// Used for testing path exploits +async function pathExploitExampleZip() { + const zip = new JSZip(); + zip.addFile("../Hello.txt", "Hello World\n"); + + const tempFileName = await Deno.makeTempFile({ + suffix: ".zip" + }); + + await zip.writeZip(tempFileName); + return tempFileName; +} + async function fromDir(dir: string, f: () => Promise) { const cwd = Deno.cwd(); Deno.chdir(dir); @@ -94,4 +108,22 @@ Deno.test("unzip without dir", async () => { const smile = await Deno.readFile(join(dir, "images", "smile.gif")); assertEquals("", decode(smile)); +}); + +Deno.test("unzip exploit test", async () => { + const dir = await Deno.makeTempDir(); + const unpackDir = join(dir, "unpack"); + await Deno.mkdir(unpackDir); + + const zipFile = await pathExploitExampleZip(); + const z = await readZip(zipFile); + + await Deno.remove(zipFile); + + assertThrowsAsync(async () => await z.unzip(unpackDir)); + assert(!(await exists(join(dir, "Hello.txt")))); + + await Deno.remove(dir, { + recursive: true + }); }); \ No newline at end of file