Skip to content

build(security): stop running dependency lifecycle scripts on install - #1317

Merged
mrbobbytables merged 1 commit into
mainfrom
sec/fix-npmrc-ignore-scripts
Oct 11, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
sec/fix-npmrc-ignore-scripts

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

Closes #1316

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 sit in
jobs holding a write-scoped GITHUB_TOKEN:

workflow permissions
architecture-submission.yml contents: write, pull-requests: write, issues: write
import-architectures.yml contents: write, pull-requests: write
pr-queue-hygiene.yml pull-requests: write, issues: write
create-milestones.yml issues: write, 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 the
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.

What this changes

  • .npmrc (new): ignore-scripts=true, with a comment recording why and
    what was checked. One file covers all eleven npm ci call sites and local
    installs, and needs no workflows permission to land.
  • CONTRIBUTING.md: one paragraph in Development setup so a contributor
    who hits a dependency that wants an install script raises it in review rather
    than quietly deleting the setting.

Scope claimed: .npmrc, and the Development setup section of
CONTRIBUTING.md. No .github/workflows/** file is touched, so this is
disjoint from #1309 (codeql.yml language set) and #1310 (automation-workflow
concurrency: groups).

Why nothing breaks

package.json declares no lifecycle hooks, and only three dependencies declare
an install script, none load-bearing:

  • @swc/core — postinstall emits an advisory warning only; the native binding
    ships prebuilt in @swc/core-linux-x64-gnu, which declares no install script.
  • core-js — postinstall prints a funding banner.
  • fsevents — macOS-only and optional; chokidar falls back without it.

Verified at cc447d8: with the file in place, npm ci resolves all 1490
packages and npm run build reports [SUCCESS] Generated static files in "build". npm run test:unit gives a byte-identical pass/fail split with and
without the file (72 files failing in the review sandbox for an unrelated
pre-existing reason — Node v26 against the declared node: 22.x), so this moves
no test either way.


Filed by sec-check agent (ACMM L4/L5 — hold-gated mode). Hold-gated: human review required.

— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88

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 <sec-check@hive.kubestellar.io>
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Important

Held for human review by the hive's ACMM level gate.

This PR was opened by the "sec-check" agent while Hive policy required a human checkpoint for that agent. Non-outreach agents are held at ACMM L3–L5; the outreach agent is always held because it publishes project-facing communication.

Hive will keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

@mrbobbytables
mrbobbytables added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 92cede9 Oct 11, 2026
5 checks passed
@mrbobbytables
mrbobbytables deleted the sec/fix-npmrc-ignore-scripts branch October 12, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] npm ci runs lifecycle scripts from 1490 packages in write-scoped CI jobs: add .npmrc with ignore-scripts=true

1 participant