From 3cdefeb2a00fe888a5ec0f24f299dca12f873d1f Mon Sep 17 00:00:00 2001 From: sec-check Date: Sun, 11 Oct 2026 10:58:49 -0400 Subject: [PATCH] build(security): stop running dependency lifecycle scripts on install A bare `npm ci` runs preinstall/install/postinstall from every package in the tree -- 1490 installed, 1557 lockfile entries -- as arbitrary code on the runner. Eleven call sites across ten workflows do exactly that, and four of them sit in jobs holding a write-scoped GITHUB_TOKEN: architecture-submission (contents/pull-requests/issues: write), import-architectures (contents/ pull-requests: write), pr-queue-hygiene and create-milestones (issues/ pull-requests: write). Those scripts run before any validator, test or lint step, so a single compromised transitive dependency gets code execution next to a token that pushes branches and opens pull requests, and can rewrite the node_modules those later steps load. SHA-pinned actions, an integrity-complete lockfile and persist-credentials: false do not cover this: none of them stop a lifecycle script inside a package the lockfile resolves faithfully. A root .npmrc covers all eleven call sites, and local installs, in one file -- and needs no workflows permission to land. Nothing here depends on an install script: package.json declares no lifecycle hooks, and the only three dependencies that declare one are not load-bearing. @swc/core's postinstall only emits an advisory warning, with the native binding shipped prebuilt in @swc/core-linux-x64-gnu, which declares no install script; core-js prints a funding banner; fsevents is macOS-only and optional, and chokidar falls back without it. Verified at cc447d8: npm ci resolves all 1490 packages and npm run build reports "Generated static files". npm run test:unit gives an identical result with and without the file. Signed-off-by: sec-check --- .npmrc | 12 ++++++++++++ CONTRIBUTING.md | 6 ++++++ 2 files changed, 18 insertions(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..233ac8e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1,12 @@ +# Dependency lifecycle scripts (preinstall/install/postinstall) are arbitrary +# code from 1490 third-party packages, and CI runs `npm ci` in jobs that hold a +# write-scoped GITHUB_TOKEN (architecture-submission, import-architectures, +# pr-queue-hygiene, create-milestones). Nothing in this project needs them, so +# they are not run. Verified: no package.json lifecycle hooks exist, and only +# three dependencies declare install scripts, none of them load-bearing: +# - @swc/core postinstall is an advisory warning only; the native +# binding ships prebuilt in @swc/core-linux-x64-gnu, which +# declares no install script of its own. +# - core-js postinstall prints a funding banner. +# - fsevents macOS-only and optional; chokidar falls back without it. +ignore-scripts=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 809e32d2..ebc7babe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,12 @@ npm run docus:start matching CI. Reach for `npm install ` only to add or upgrade a dependency, and commit the resulting lockfile change deliberately. +The repository's `.npmrc` sets `ignore-scripts=true`, so neither command runs +dependency lifecycle scripts. That is deliberate: CI installs this tree in jobs +that hold a write-scoped `GITHUB_TOKEN`, and no dependency here needs an install +script to work. If a future dependency genuinely does, say so in the pull +request rather than dropping the setting. + The dev server runs at `http://localhost:3000`. In the devcontainer: ```bash