CVE-2022-48285
Description
JSZip 3.7.1 and earlier allow directory traversal via crafted ZIP, enabling arbitrary file overwrite extracted by loadAsync.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
JSZip 3.7.1 and earlier allow directory traversal via crafted ZIP, enabling arbitrary file overwrite extracted by loadAsync.
Vulnerability
CVE-2022-48285 is a directory traversal vulnerability in JSZip, a JavaScript library for creating and reading ZIP archives. The loadAsync method prior to version 3.8.0 does not sanitize filenames inside a ZIP archive, allowing an attacker to include path traversal sequences (e.g., ../). This is a classic "Zip Slip" flaw [1].
Exploitation
An attacker can craft a malicious ZIP file containing entries with filenames like ../../malicious.js. When a victim application calls loadAsync on this archive, JSZip would extract the file to a directory outside the intended extraction root, potentially overwriting sensitive files. No authentication is required beyond the ability to deliver the crafted ZIP to the target application [1][2].
Impact
Successful exploitation could allow an attacker to overwrite arbitrary files on the server or client system, depending on where the extraction occurs. This could lead to remote code execution if critical files (e.g., application scripts, configuration files) are replaced with attacker-controlled content. The vulnerability is rated high severity with a CVSS score of 7.5 [1].
Mitigation
The issue is fixed in JSZip 3.8.0. The commit [3] introduces a resolve function that normalizes path components, stripping relative traversal sequences (., ..) that would escape the root. Applications using JSZip should update to version 3.8.0 or later to prevent exploitation. No workaround is available for older versions.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
jszipnpm | < 3.8.0 | 3.8.0 |
Affected products
2- JSZip/JSZipdescription
Patches
12edab366119cSanitize filenames with `loadAsync` to prevent zip slip attacks
9 files changed · +113 −15
CHANGES.md+5 −1 modified@@ -4,10 +4,14 @@ layout: default section: main --- +### v3.8.0 2022-03-30 + +- Santize filenames when files are loaded with `loadAsync`, to avoid ["zip slip" attacks](https://snyk.io/research/zip-slip-vulnerability). The original filename is available on each zip entry as `unsafeOriginalName`. See the [documentation](https://stuk.github.io/jszip/documentation/api_jszip/load_async.html). Many thanks to McCaulay Hudson for reporting. + ### v3.7.1 2021-08-05 - Fix build of `dist` files. - + Note: this version ensures the changes from 3.7.0 are actually included in the `dist` files. Thanks to Evan W for reporting. + + Note: this version ensures the changes from 3.7.0 are actually included in the `dist` files. Thanks to Evan W for reporting. ### v3.7.0 2021-07-23
dist/jszip.js+37 −5 modified@@ -1,6 +1,6 @@ /*! -JSZip v3.7.1 - A JavaScript class for generating and reading zip files +JSZip v3.8.0 - A JavaScript class for generating and reading zip files <http://stuartk.com/jszip> (c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com> @@ -1059,7 +1059,7 @@ JSZip.defaults = require('./defaults'); // TODO find a better way to handle this version, // a require('package.json').version doesn't work with webpack, see #327 -JSZip.version = "3.7.1"; +JSZip.version = "3.8.0"; JSZip.loadAsync = function (content, options) { return new JSZip().loadAsync(content, options); @@ -1132,7 +1132,11 @@ module.exports = 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, @@ -1142,6 +1146,9 @@ module.exports = 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; @@ -3352,6 +3359,31 @@ exports.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. + */ +exports.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. @@ -3460,8 +3492,8 @@ exports.prepareContent = function(name, inputData, isBinary, isOptimizedBinarySt // if inputData is already a promise, this flatten it. var promise = external.Promise.resolve(inputData).then(function(data) { - - + + var isBlob = support.blob && (data instanceof Blob || ['[object File]', '[object Blob]'].indexOf(Object.prototype.toString.call(data)) !== -1); if (isBlob && typeof FileReader !== "undefined") {
dist/jszip.min.js+2 −2 modifieddocumentation/api_jszip/load_async.md+23 −0 modified@@ -10,6 +10,8 @@ object at the current folder level. This technique has some limitations, see If the JSZip object already contains entries, new entries will be merged. If two have the same name, the loaded one will replace the other. +Since v3.8.0 this method will santize relative path components (i.e. `..`) in loaded filenames to avoid ["zip slip" attacks](https://snyk.io/research/zip-slip-vulnerability). For example: `../../../example.txt` → `example.txt`, `src/images/../example.txt` → `src/example.txt`. The original filename is available on each zip entry as `unsafeOriginalName`. + __Returns__ : A [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with the updated zip object. The promise can fail if the loaded data is not valid zip data or if it uses unsupported features (multi volume, password protected, etc). @@ -194,3 +196,24 @@ zip.loadAsync(bin1) // file3.txt, from bin2 }); ``` + +Reading a zip file with relative filenames: + +```js +// here, "unsafe.zip" is zip file containing: +// src/images/../file.txt +// ../../example.txt + +require("fs").readFile("unsafe.zip", function (err, data) { + if (err) throw err; + var zip = new JSZip(); + zip.loadAsync(data) + .then(function (zip) { + console.log(zip.files); + // src/file.txt + // example.txt + console.log(zip.files["example.txt"].unsafeOriginalName); + // "../../example.txt" + }); +} +```
index.d.ts+5 −0 modified@@ -65,6 +65,11 @@ declare namespace JSZip { 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;
lib/index.js+1 −1 modified@@ -45,7 +45,7 @@ JSZip.defaults = require('./defaults'); // TODO find a better way to handle this version, // a require('package.json').version doesn't work with webpack, see #327 -JSZip.version = "3.7.1"; +JSZip.version = "3.8.0"; JSZip.loadAsync = function (content, options) { return new JSZip().loadAsync(content, options);
lib/load.js+8 −1 modified@@ -61,7 +61,11 @@ module.exports = 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, @@ -71,6 +75,9 @@ module.exports = 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;
lib/utils.js+27 −2 modified@@ -317,6 +317,31 @@ exports.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. + */ +exports.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. @@ -425,8 +450,8 @@ exports.prepareContent = function(name, inputData, isBinary, isOptimizedBinarySt // if inputData is already a promise, this flatten it. var promise = external.Promise.resolve(inputData).then(function(data) { - - + + var isBlob = support.blob && (data instanceof Blob || ['[object File]', '[object Blob]'].indexOf(Object.prototype.toString.call(data)) !== -1); if (isBlob && typeof FileReader !== "undefined") {
test/asserts/utils.js+5 −3 modified@@ -1,19 +1,21 @@ /* global QUnit,JSZip,JSZipTestUtils */ 'use strict'; +// These tests only run in Node var utils = require("../../lib/utils"); QUnit.module("utils"); QUnit.test("Paths are resolved correctly", function (assert) { - assert.strictEqual(utils.resolve("root\\a\\b"), "root/a/b"); + // Backslashes can be part of filenames + assert.strictEqual(utils.resolve("root\\a\\b"), "root\\a\\b"); assert.strictEqual(utils.resolve("root/a/b"), "root/a/b"); assert.strictEqual(utils.resolve("root/a/.."), "root"); assert.strictEqual(utils.resolve("root/a/../b"), "root/b"); assert.strictEqual(utils.resolve("root/a/./b"), "root/a/b"); assert.strictEqual(utils.resolve("root/../../../"), ""); - assert.strictEqual(utils.resolve("////"), ""); - assert.strictEqual(utils.resolve("/a/b/c"), "a/b/c"); + assert.strictEqual(utils.resolve("////"), "/"); + assert.strictEqual(utils.resolve("/a/b/c"), "/a/b/c"); assert.strictEqual(utils.resolve("a/b/c/"), "a/b/c/"); assert.strictEqual(utils.resolve("../../../../../a"), "a"); assert.strictEqual(utils.resolve("../app.js"), "app.js");
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-36fh-84j7-cv5hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-48285ghsaADVISORY
- exchange.xforce.ibmcloud.com/vulnerabilities/244499ghsaWEB
- github.com/Stuk/jszip/commit/2edab366119c9ee948357c02f1206c28566cdf15ghsaWEB
- github.com/Stuk/jszip/compare/v3.7.1...v3.8.0ghsaWEB
- security.netapp.com/advisory/ntap-20240621-0005ghsaWEB
- www.mend.io/vulnerability-database/WS-2023-0004ghsaWEB
- security.netapp.com/advisory/ntap-20240621-0005/mitre
News mentions
0No linked articles in our index yet.