Skip to content

dogfood: a fresh scaffold reports 5 high-severity npm audit advisories #1418

Description

@vivek7405

Problem

Every freshly scaffolded app reports 5 high-severity advisories the moment webjs create finishes its npm install:

5 high severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

That is the first thing an agent or a developer sees after creating an app, and it is the last impression the scaffold should leave. All 5 are one advisory reached through one dependency chain:

@web/test-runner@0.20.2
  -> @web/test-runner-chrome@0.18.1
    -> puppeteer-core@24.43.1
      -> @puppeteer/browsers@2.13.2
        -> extract-zip@2.0.1

The advisory is GHSA-jmr9-qjv8-65gv, an unvalidated symlink path traversal in extract-zip. Two things make this straightforward rather than a real exposure. It is a devDependency, never reachable from the app runtime. And the scaffold does not use that code path at all: templates/web-test-runner.config.js launches browsers through playwrightLauncher({ product: 'chromium' }) from @web/test-runner-playwright, so the puppeteer chrome launcher is dead weight that @web/test-runner pulls in unconditionally through its core dependency on @web/test-runner-chrome.

Design / approach

extract-zip has no patched version. The advisory covers *, and the newest published release is 2.0.1, so an override on extract-zip itself cannot resolve anything.

The fix comes one level up. @puppeteer/browsers@3.x dropped extract-zip entirely (it unpacks with modern-tar now), and puppeteer-core@25.x depends on @puppeteer/browsers@^3. So bumping puppeteer-core past the 24 line clears the whole chain:

puppeteer-core@24.43.1 deps: { "@puppeteer/browsers": "2.13.2", ... }   <- exact pin, vulnerable
puppeteer-core@25.7.0  deps: { "@puppeteer/browsers": "3.2.0",  ... }   <- clean

puppeteer-core@24.43.1 pins @puppeteer/browsers exactly ("2.13.2", no caret), so npm cannot dedupe its way out. It needs an explicit overrides entry in the generated package.json:

"overrides": {
  "puppeteer-core": "^25.7.0"
}

Verified end to end against a real scaffold (npm create webjs@latest, then the override applied and reinstalled):

  • npm audit goes from 5 high severity vulnerabilities to found 0 vulnerabilities.
  • npm test passes: 8 tests across the server and browser layers, 0 failed, browser tests launching Chromium through Playwright exactly as before.

Rejected alternative: bump @web/test-runner to ^1.0.0

This is what npm audit fix --force proposes ("Will install @web/test-runner@1.0.0, which is a breaking change"), and it does not fix the advisory. @web/test-runner@1.0.0 depends on @web/test-runner-chrome@^1.0.0, which still declares "puppeteer-core": "^24.0.0", so the same vulnerable chain resolves. Taking a breaking major that leaves the audit red is strictly worse than the override. Do not do this as part of this issue; if the v1 line is wanted it is separate work with its own justification.

Why an override rather than dropping the dependency

@web/test-runner-chrome is a hard dependency of @web/test-runner itself, not an optional launcher the scaffold opts into. There is no supported way to install the runner without it, so the choice is between overriding the transitive version and living with the advisory. The override is narrow, it names one package, and it targets code the scaffold never executes.

Implementation notes (for the implementing agent)

Where to edit

  • packages/cli/lib/create.js, the manifest object inside scaffoldApp(). devDependencies is at roughly L445 to L478 and @web/test-runner is declared at L453. Add a sibling overrides key immediately after the devDependencies object closes at L478, before the webjs config block that starts at L479. Follow the surrounding style: the manifest is heavily commented, and every non-obvious pin in it carries a comment explaining the reasoning (see the drizzle-orm exact-pin comment at L435 to L438 for the house shape). The comment here must record that extract-zip has no patched version, that the fix works by moving @puppeteer/browsers to the 3 line, and that the scaffold launches Playwright rather than the puppeteer chrome launcher, so a future reader does not delete the override as unexplained cruft.
  • The override is unconditional across both templates. devDependencies declares @web/test-runner outside any isApi branch (contrast L463, where @tailwindcss/cli is full-stack only), so both full-stack and api carry the chain today and both need the override.

Landmines / gotchas

  • Do not write the override against extract-zip. It reads like the obvious target and it cannot work, because every published version is covered by the advisory. The override has to sit at puppeteer-core, which is the nearest ancestor with a clean release.
  • Do not reach for npm audit fix --force. See the rejected alternative above. It takes a breaking major and leaves the audit red.
  • @web/test-runner-chrome@0.18.1 declares "puppeteer-core": "^24.0.0", so the override deliberately forces a version outside its declared range. That is fine here, and it is exactly why the verification step below runs the real browser suite rather than trusting the resolution: the chrome launcher is never constructed by the scaffold config, but the override must be proven not to break the install or the runner's module graph.
  • webjs create runs npm install as part of scaffolding, so the change is exercised on the very next webjs create. Verify against a genuinely fresh scaffold in a scratch directory, not a pre-existing one whose node_modules and lockfile predate the change.
  • Pin the override with a caret (^25.7.0), not exactly. This is a security floor, not a reproducibility pin, so a generated app should pick up later 25.x patches. That is the opposite of the drizzle-orm reasoning at L435, where an exact pin is deliberate because the scaffold source is written against a specific rc API.

Invariants to respect

  • Scaffold changes are scaffold changes: nothing here touches the framework packages or their runtime. packages/cli is plain .js with JSDoc (root AGENTS.md, "Working in the WebJs framework repo itself"), so no .ts file enters packages/.
  • packages/cli/AGENTS.md invariant 1: exactly three templates. This change adds no template and no flag, it only edits the shared manifest.

Tests + docs surfaces

  • test/scaffolds/scaffold-integration.test.js already asserts the generated manifest's dependency shape (see L343 to L363, which check @tailwindcss/cli, @webjsdev/intellisense, @types/node, and the absence of ts-lit-plugin). Add an assertion in that neighbourhood that the generated package.json carries overrides['puppeteer-core'], and assert it for both templates, since the manifest is shared and a future isApi branch could silently drop it from one.
  • Note the honest limit of that assertion: it proves the override is emitted, not that the audit is clean, because a unit test cannot install from the network. The clean-audit half is a manual verification step, recorded in the PR body: scaffold a fresh app, run npm audit and confirm zero, then run npm test and confirm the browser suite still launches Chromium and passes.
  • The counterfactual is the pre-change state, which is already measured: without the override the same scaffold reports 5 high-severity advisories. Record both numbers in the PR.
  • Docs: no public API surface changes, so there is nothing to sync into the docs site, the marketing website, AGENTS.md, or the agent skill. The explanatory comment in create.js is the documentation for this one.

Acceptance criteria

  • A freshly scaffolded app's package.json carries "overrides": { "puppeteer-core": "^25.7.0" }, for the full-stack template and the api template alike
  • npm audit in a fresh scaffold reports found 0 vulnerabilities, down from 5 high severity
  • npm test in that fresh scaffold still passes, with browser tests launching Chromium through Playwright
  • test/scaffolds/scaffold-integration.test.js asserts the override is emitted for both templates
  • The override carries a comment in create.js recording why it exists, why extract-zip cannot be the target, and why the puppeteer chrome launcher is unused
  • The manual audit verification (before and after counts) is recorded in the PR body

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions