Skip to content

Commit

Permalink
fixed exploit of unzip method, using rel paths
Browse files Browse the repository at this point in the history
  • Loading branch information
leopf committed Jul 4, 2021
1 parent fd7453f commit 7e21f5b
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 16 deletions.
34 changes: 19 additions & 15 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _JSZip from "https://dev.jspm.io/[email protected]";
import _JSZip from "https://dev.jspm.io/[email protected]";
import { WalkOptions, walk } from "https://deno.land/[email protected]/fs/walk.ts";
import { SEP, resolve, dirname } from "https://deno.land/[email protected]/path/mod.ts";
import { SEP, resolve, dirname, globToRegExp } from "https://deno.land/[email protected]/path/mod.ts";
import type {
InputFileFormat,
JSZipAddFileOptions,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*
Expand All @@ -196,22 +196,26 @@ export class JSZip {
async unzip(dir: string = "."): Promise<void> {
// FIXME optionally replace the existing folder prefix with dir.
const createdDirs = new Set<string>();
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);
}
}
}

Expand Down
34 changes: 33 additions & 1 deletion test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { decode, encode } from "https://deno.land/[email protected]/encoding/utf8.ts";
import { join } from "https://deno.land/[email protected]/path/mod.ts";
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
import { assert, assertEquals, assertThrowsAsync } from "https://deno.land/[email protected]/testing/asserts.ts";
import { JSZip, readZip, zipDir } from "./mod.ts";
import { exists } from "https://deno.land/[email protected]/fs/mod.ts";

// FIXME use tmp directory and clean up.
async function exampleZip(path: string, createDirectories = true) {
Expand All @@ -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<T>(dir: string, f: () => Promise<T>) {
const cwd = Deno.cwd();
Deno.chdir(dir);
Expand Down Expand Up @@ -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
});
});

0 comments on commit 7e21f5b

Please sign in to comment.