Skip to content

ci: npm-publish trusted publishing has no environment/branch gate, so workflow_dispatch can publish arbitrary content #234

Description

@beardthelion

Summary

Since #233, .github/workflows/release.yml pairs an unrestricted workflow_dispatch trigger with npm Trusted Publishing (id-token: write on npm-publish) and no GitHub Environment protection. That combination lets any write-access collaborator publish arbitrary content to the @gitlawb/* packages under the repo's OIDC identity, bypassing the review-gated main merge.

Why it works

  1. release.yml on main has a bare workflow_dispatch: (no branch filter), id-token: write on npm-publish, and no environment: on any job.
  2. workflow_dispatch runs the workflow file from the selected ref, not from main. A collaborator can push a feature branch with a modified release.yml and dispatch it against that branch.
  3. npm's trusted publisher matches on org + repo + workflow filename only (plus an optional environment); the git ref is not part of the trust. So a token minted from the attacker's branch is accepted, and the modified workflow publishes to npm with provenance attesting to it.

Requiring the workflow to exist on the default branch does not help (it only needs to exist there, not to be the version that runs), and branch protection on main does not cover the dispatched feature branch.

Reachability: any collaborator with write access (an insider or a compromised token), not anonymous.

Fix

Add a protected GitHub Environment (required reviewers or a main-only deployment-branch rule) for npm-publish, set environment: on the job, and scope the npmjs trusted publisher to that environment. Optionally also gate dispatch on github.ref == 'refs/heads/main' (necessary but not sufficient on its own, since branch content can edit the gate).

Note: if an environment is already configured on the npmjs side but not declared in the workflow, publishing would fail closed rather than be exploitable. Either way it needs the environment: declaration to line up.

Minor hardening in the same job

  • The backfill tag validation case "$TAG" in v[0-9]*) matches across newlines (a multiline input injects extra $GITHUB_OUTPUT lines) and accepts non-tag refs. Anchor it to exact semver, reject CR/LF, and gh release view "$TAG" before checkout.
  • npm install -g npm@^11.5.1 floats to the latest 11.x in the publish job; pin an exact version and assert npm --version satisfies it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:securityVulnerability fix or hardeningsev:highMajor break or real security/trust risk, no easy workaround

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions