diff --git a/index.d.ts b/index.d.ts
index da00026..458458d 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -51,6 +51,11 @@ export type OutputType = keyof OutputByType;
export interface JSZipObject {
name: string;
+ /**
+ * Present for files loadded with `loadAsync`. May contain ".." path components that could
+ * result in a zip-slip attack. See https://snyk.io/research/zip-slip-vulnerability
+ */
+ unsafeOriginalName?: string;
dir: boolean;
date: Date;
comment: string;
diff --git a/lib/load.js b/lib/load.js
index 6c27ae9..a4dec77 100644
--- a/lib/load.js
+++ b/lib/load.js
@@ -62,7 +62,11 @@ export default function(data, options) {
var files = zipEntries.files;
for (var i = 0; i < files.length; i++) {
var input = files[i];
- zip.file(input.fileNameStr, input.decompressed, {
+
+ var unsafeName = input.fileNameStr;
+ var safeName = utils.resolve(input.fileNameStr);
+
+ zip.file(safeName, input.decompressed, {
binary: true,
optimizedBinaryString: true,
date: input.date,
@@ -72,6 +76,9 @@ export default function(data, options) {
dosPermissions : input.dosPermissions,
createFolders: options.createFolders
});
+ if (!input.dir) {
+ zip.file(safeName).unsafeOriginalName = unsafeName;
+ }
}
if (zipEntries.zipComment.length) {
zip.comment = zipEntries.zipComment;
diff --git a/lib/utils.js b/lib/utils.js
index 29ae8fa..c3203ac 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -254,6 +254,31 @@ export const transformTo = function(outputType, input) {
return result;
};
+/**
+ * Resolve all relative path components, "." and "..", in a path. If these relative components
+ * traverse above the root then the resulting path will only contain the final path component.
+ *
+ * All empty components, e.g. "//", are removed.
+ * @param {string} path A path with / or \ separators
+ * @returns {string} The path with all relative path components resolved.
+ */
+export const resolve = function(path) {
+ var parts = path.split("/");
+ var result = [];
+ for (var index = 0; index < parts.length; index++) {
+ var part = parts[index];
+ // Allow the first and last component to be empty for trailing slashes.
+ if (part === "." || (part === "" && index !== 0 && index !== parts.length - 1)) {
+ continue;
+ } else if (part === "..") {
+ result.pop();
+ } else {
+ result.push(part);
+ }
+ }
+ return result.join("/");
+};
+
/**
* Return the type of the input.
* The type will be in a format valid for JSZip.utils.transformTo : string, array, uint8array, arraybuffer.
diff --git a/test/asserts/utils.js b/test/asserts/utils.js
new file mode 100644
index 0000000..c9987cc
--- /dev/null
+++ b/test/asserts/utils.js
@@ -0,0 +1,36 @@
+/* global QUnit,JSZip,JSZipTestUtils,Promise */
+'use strict';
+
+QUnit.module("utils");
+
+function resolve(path) {
+ var parts = path.split("/");
+ var result = [];
+ for (var index = 0; index < parts.length; index++) {
+ var part = parts[index];
+ // Allow the first and last component to be empty for trailing slashes.
+ if (part === "." || (part === "" && index !== 0 && index !== parts.length - 1)) {
+ continue;
+ } else if (part === "..") {
+ result.pop();
+ } else {
+ result.push(part);
+ }
+ }
+ return result.join("/");
+};
+
+QUnit.test("Paths are resolved correctly", function (assert) {
+ // Backslashes can be part of filenames
+ assert.strictEqual(resolve("root\\a\\b"), "root\\a\\b");
+ assert.strictEqual(resolve("root/a/b"), "root/a/b");
+ assert.strictEqual(resolve("root/a/.."), "root");
+ assert.strictEqual(resolve("root/a/../b"), "root/b");
+ assert.strictEqual(resolve("root/a/./b"), "root/a/b");
+ assert.strictEqual(resolve("root/../../../"), "");
+ assert.strictEqual(resolve("////"), "/");
+ assert.strictEqual(resolve("/a/b/c"), "/a/b/c");
+ assert.strictEqual(resolve("a/b/c/"), "a/b/c/");
+ assert.strictEqual(resolve("../../../../../a"), "a");
+ assert.strictEqual(resolve("../app.js"), "app.js");
+});
diff --git a/test/index.html b/test/index.html
index e29f79f..ee26a1f 100644
--- a/test/index.html
+++ b/test/index.html
@@ -63,6 +63,7 @@
+