Skip to content

LTS-4750: drop extract-zip via @puppeteer/browsers 3.x override (GHSA-jmr9-qjv8-65gv) - #22

Merged
SaranshBS merged 1 commit into
mainfrom
LTS-4750-extract-zip-path-traversal
Aug 14, 2026
Merged

LTS-4750: drop extract-zip via @puppeteer/browsers 3.x override (GHSA-jmr9-qjv8-65gv)#22
SaranshBS merged 1 commit into
mainfrom
LTS-4750-extract-zip-path-traversal

Conversation

@MihirR-BS

Copy link
Copy Markdown
Contributor

Jira: https://browserstack.atlassian.net/browse/LTS-4750
Advisory: GHSA-jmr9-qjv8-65gv / CVE-2026-56876 — extract-zip unvalidated symlink path traversal, CVSS 8.1 (High), CWE-22
Dependabot alert: #143

RCA

extract-zip is abandoned. The advisory range is <= 2.0.1 and 2.0.1 is latest (published 2020), so the API reports first_patched_version: null — there is no version to bump to and Dependabot could not open a PR.

The single copy in this tree is pulled transitively:

@wdio/cli / @wdio/local-runner / @wdio/mocha-framework
  └─ @wdio/utils@9.29.1
       └─ @puppeteer/browsers@2.13.2   (declares extract-zip ^2.0.1)
            └─ extract-zip@2.0.1

npm audit reported 12 high before this change. All 12 are the same extract-zip root re-reported up the @wdio/* "depends on vulnerable versions of" chain — @puppeteer/browsers, @wdio/utils, @wdio/config, webdriver, webdriverio, expect-webdriverio, @wdio/globals, @wdio/runner, @wdio/local-runner, @wdio/mocha-framework, @wdio/cli. GitHub agrees: exactly one open Dependabot alert (#143).

npm's own suggested remediation is npm audit fix --force → downgrade the whole suite to @wdio/cli@8.14.6 / @wdio/mocha-framework@8.14.0. That is a major downgrade of the product this repo demonstrates, and not acceptable.

Fix

@puppeteer/browsers 3.x removed the extract-zip dependency and ships its own extractor, which validates symlink targets before creating them (src/fileUtil.ts):

const linkTarget = Buffer.concat(chunks).toString();
// Verify that the link does not resolve outside of the target directory.
const resolvedLinkTarget = path.resolve(path.dirname(destination), linkTarget);
if (!isInsideDirectory(folderPath, resolvedLinkTarget)) {
  throw new Error(`Zip symlink "${entry.fileName}" would point outside of the target directory.`);
}
await symlink(linkTarget, destination);

So this is not a swap of one unvalidated extractor for another — it is the fix for this exact CWE-22, and the vulnerable package leaves the tree entirely.

   "overrides": {
+    "@puppeteer/browsers": "^3.1.0",

3.1.0 (published 2026-08-04) rather than 3.2.0 (2026-08-11) so this repo's .npmrc min-release-age=7 supply-chain cooldown is respected. The ^3.1.0 range lets the lock float to 3.2.x once it clears the cooldown, per the caret convention adopted in #21.

Tree delta

Removed: extract-zip, @types/yauzl, fd-slicer, progress, yauzl@2.10.0, buffer-crc32@0.2.13.

3.x also demotes proxy-agent from a hard dependency to a peer (>=8.0.1), so npm resolves a refreshed proxy-agents subtree: proxy-agent 6.5.0 → 8.0.2, pac-proxy-agent/pac-resolver/https-proxy-agent/http-proxy-agent → 9.x, degenerator → 7.0.1, socks-proxy-agent → 10.1.0, get-uri → 8.0.1, plus three newly-introduced packages. Provenance checked, since this is a security PR:

package maintainer repo
quickjs-wasi tootallnate, adamdong vercel-labs/quickjs-wasi
proxy-agent-negotiate tootallnate TooTallNate/proxy-agents
get-east-asian-width sindresorhus sindresorhus/get-east-asian-width

All MIT, all from the maintainers of the packages that pull them, and none carry an advisory.

engines.node >=18>=22.12.0

@puppeteer/browsers 3.x is ESM-only and declares "node": ">=22.12.0" itself. With this repo's engine-strict=true, a stale root floor of >=18 means a customer on Node 20 gets an EBADENGINE hard error from a transitive dependency instead of a clear message from us — so the root floor has to agree. Node 18 (EOL 2025-04-30) and Node 20 (EOL 2026-04-30) are both out of support as of today; Node 22 is Active LTS. .npmrc comment updated to record the reason.

Verification

1. Audit — 12 high → 0 high (clean npm ci, 523 top-level entries):

before: 18 vulnerabilities (6 moderate, 12 high)
after:   6 moderate severity vulnerabilities

The 6 moderate are pre-existing and untouched: qs / uuid via googleapisbrowserstack-node-sdk, unticketed and below this repo's own .npmrc audit-level=high gate — same call as #20 and #21.

2. extract-zip is gone from the installed treefind node_modules -name extract-zip returns nothing; npm ls shows @puppeteer/browsers@3.1.0 overridden.

3. PoC, before and after, on the same malicious archive. Built a zip carrying a traversal symlink (pwned -> ../../../../../../../../../../tmp/lts4750-escape, entry externalFileAttributes 0xa1ed0000S_IFLNK) and extracted it with both:

VULNERABLE  extract-zip@2.0.1 wrote pwned -> ../../../../../../../../../../tmp/lts4750-escape
            resolves to /tmp/lts4750-escape  (outside /var/folders/.../T/poc-jYlWQ2)
FIXED       @puppeteer/browsers@3.1.0 rejected the traversal symlink

3.1.0 throws ... would point outside of the target directory and leaves no symlink behind.

4. Real code path, end to end. npx wdio run exercises @wdio/utilssetupChromedriver()@puppeteer/browsers.install(), and Chromedriver ships as a .zip — precisely the archive type that used to route through extract-zip:

INFO @wdio/utils: Setting up browser driver for: chrome@stable
INFO webdriver: Downloading Chromedriver v150.0.7871.189
INFO webdriver: Download of Chromedriver v150.0.7871.189 was successful
INFO chromedriver: ChromeDriver was started successfully on port 54924.
INFO webdriver: Initiate new session using the WebDriver protocol
[chrome 150.0.7871.189 mac]    ✓ should add product to cart successfully

Live WebDriver session against bstackdemo.com, add-to-cart spec passing. Also replayed the exact resolveBuildIdcanDownloadinstallcomputeExecutablePath sequence @wdio/utils uses and asserted the unpacked binary reports the right version and lands inside cacheDir.

Reachability

Honest read: the local-browser path is reachable in this repo (wdio.conf.js has runner: 'local' with plain chrome capabilities, so a bare npm test downloads and unpacks Chromedriver locally), but the archive comes from Google's storage-for-testing CDN over TLS, not from anywhere an attacker controls. Exploitation needs a compromised or MITM'd CDN response. So: latent, not exploitable as this repo uses it — patched anyway, per this repo's standing precedent of fixing dev-only transitive CVEs through overrides rather than dismissing them (#20, #21, LTS-2594/2753/3158/3159/3411/3907/4206, LTS-4004, LTS-2886, LTS-3295).

Out of scope — pre-existing bug worth a follow-up

npm test fails on main today, before this PR, unrelated to it:

Error: Couldn't find plugin "chromedriver" service, neither as wdio scoped package
"@wdio/chromedriver-service" nor as community package "wdio-chromedriver-service".

wdio.conf.js:34 still carries services: ['chromedriver'], but wdio-chromedriver-service is not in devDependencies and is not in the lockfile on main either — WDIO 9 handles driver management inside @wdio/utils, so the entry is stale and should just be dropped. Deliberately not touched here to keep a security PR free of behaviour changes; the verification runs above used a scratch copy of the config with that one line removed. Filing separately.

Also noted: the "puppeteer-core": { "tar-fs": "^3.0.7" } override is now vestigial (puppeteer-core is an optional peer of webdriverio and is not installed). Left alone for the same reason.

…-jmr9-qjv8-65gv)

extract-zip is abandoned — the advisory range is `<= 2.0.1` and 2.0.1 is
`latest` (published 2020), so `first_patched_version` is null. There is no
version to bump to.

The only copy in this tree comes from `@wdio/utils` -> `@puppeteer/browsers`
`^2.2.0`. `@puppeteer/browsers` 3.x replaced extract-zip with its own
extractor, which validates symlink targets before creating them:

  if (!isInsideDirectory(folderPath, resolvedLinkTarget)) {
    throw new Error(`Zip symlink "..." would point outside of the target directory.`)
  }

So overriding `@puppeteer/browsers` to `^3.1.0` removes the vulnerable package
rather than papering over it. That drops extract-zip, @types/yauzl, fd-slicer,
progress and yauzl@2 from the tree and takes `npm audit` from 12 high to 0 high
(all 12 were the same extract-zip root, re-reported up the @wdio/* chain).

3.x makes `proxy-agent` a peerDependency (`>=8.0.1`) instead of a hard dep, so
npm pulls a refreshed proxy-agents subtree (proxy-agent 8.0.2, pac-* 9,
degenerator 7 + quickjs-wasi, http/https-proxy-agent 9 + proxy-agent-negotiate,
socks-proxy-agent 10). All are TooTallNate / vercel-labs / sindresorhus
packages and carry no advisories.

Pinned to 3.1.0 in the lock (published 2026-08-04) rather than 3.2.0
(2026-08-11) so the `.npmrc` `min-release-age=7` cooldown is respected;
the `^3.1.0` range lets it float once 3.2.0 clears it.

`engines.node` goes `>=18` -> `>=22.12.0`: @puppeteer/browsers 3.x is ESM-only
and sets that floor itself, and with `engine-strict=true` the root floor has to
agree or customers get the error from a transitive dep instead of from us.
Node 18 (EOL 2025-04-30) and Node 20 (EOL 2026-04-30) are both out of support.

Verified:
- `npm ci` clean: 0 high / 0 critical (was 12 high); 6 moderate remain
  (qs/uuid via googleapis <- browserstack-node-sdk), unticketed and below this
  repo's own `audit-level=high` gate.
- No `extract-zip` anywhere in the installed tree.
- PoC on a zip carrying `pwned -> ../../../../../../../../../../tmp/lts4750-escape`:
  extract-zip@2.0.1 writes the symlink and it resolves to /tmp/lts4750-escape,
  outside the extraction dir; @puppeteer/browsers@3.1.0 rejects it with
  "would point outside of the target directory" and leaves nothing behind.
- Full local run through the real code path (`npx wdio run`): Chromedriver
  v150.0.7871.189 downloaded and unpacked from a .zip by 3.1.0 (the former
  extract-zip call site), driver started, live WebDriver session against
  bstackdemo.com, add-to-cart spec passing.
@MihirR-BS
MihirR-BS requested a review from a team as a code owner August 14, 2026 09:44
@MihirR-BS
MihirR-BS requested a review from jhawarchirag August 14, 2026 09:44
@dheeren-gaud
dheeren-gaud self-requested a review August 14, 2026 09:55
@SaranshBS
SaranshBS merged commit e2cbbab into main Aug 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants