Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes the issue of missing directories, if the filepath includes backslashes #30

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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, join } 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,
JSZipFileOptions,
JSZipAddFileOptions,
JSZipGeneratorOptions,
JSZipLoadOptions,
JSZipObject,
Expand Down Expand Up @@ -107,10 +107,13 @@ export class JSZip {
addFile(
path: string,
content?: string | Uint8Array,
options?: JSZipFileOptions,
options?: JSZipAddFileOptions,
): JSZipObject {
const replaceBackslashes = options?.replaceBackslashes === undefined || options.replaceBackslashes;
const finalPath = replaceBackslashes ? path.replaceAll("\\", "/") : path;

// @ts-ignores
const f = this._z.file(path, content, options);
const f = this._z.file(finalPath, content, options);
return f as JSZipObject;
}

Expand Down Expand Up @@ -192,16 +195,27 @@ export class JSZip {
*/
async unzip(dir: string = "."): Promise<void> {
// FIXME optionally replace the existing folder prefix with dir.
for (const f of this) {
const ff = join(dir, f.name);
if (f.dir) {
// hopefully the directory is prior to any files inside it!
await Deno.mkdir(ff, { recursive: true });
continue;
const createdDirs = new Set<string>();
const allowedFileLocRegex = globToRegExp(resolve(dir, "**"));

for (const fileEntry of this) {
const filePath = resolve(dir, fileEntry.name);
if (!allowedFileLocRegex.test(filePath)) {
throw new Error("Not allowed!");
}

const dirPath = fileEntry.dir ? filePath : dirname(filePath);

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);
}
const content = await f.async("uint8array");
// TODO pass WriteFileOptions e.g. mode
await Deno.writeFile(ff, content);
}
}

Expand Down
60 changes: 56 additions & 4 deletions test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
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) {
async function exampleZip(path: string, createDirectories = true) {
const zip = new JSZip();
zip.addFile("Hello.txt", "Hello World\n");

const img = zip.folder("images");
img.addFile("smile.gif", "\0", { base64: true });
if (createDirectories) {
const img = zip.folder("images");
img.addFile("smile.gif", "\0", { base64: true });
}
else {
// Use backslashed for edge case where directory is missing.
zip.addFile("images\\smile.gif", "\0", { base64: 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 @@ -75,3 +95,35 @@ Deno.test("unzip", async () => {
const smile = await Deno.readFile(join(dir, "images", "smile.gif"));
assertEquals("", decode(smile));
});

Deno.test("unzip without dir", async () => {
const dir = await Deno.makeTempDir();

await exampleZip("example.zip", false);
const z = await readZip("example.zip");
await z.unzip(dir);

const content = await Deno.readFile(join(dir, "Hello.txt"));
assertEquals("Hello World\n", decode(content));

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
});
});
4 changes: 4 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export interface JSZipFileOptions {
unixPermissions?: number | string | null;
}

export interface JSZipAddFileOptions extends JSZipFileOptions {
replaceBackslashes?: boolean;
}

export interface JSZipGeneratorOptions<T extends OutputType = OutputType> {
compression?: Compression;
compressionOptions?: null | {
Expand Down