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
release.yml on main has a bare workflow_dispatch: (no branch filter), id-token: write on npm-publish, and no environment: on any job.
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.
- 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.
Summary
Since #233,
.github/workflows/release.ymlpairs an unrestrictedworkflow_dispatchtrigger with npm Trusted Publishing (id-token: writeonnpm-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-gatedmainmerge.Why it works
release.ymlonmainhas a bareworkflow_dispatch:(no branch filter),id-token: writeonnpm-publish, and noenvironment:on any job.workflow_dispatchruns the workflow file from the selected ref, not frommain. A collaborator can push a feature branch with a modifiedrelease.ymland dispatch it against that branch.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
maindoes 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) fornpm-publish, setenvironment:on the job, and scope the npmjs trusted publisher to that environment. Optionally also gate dispatch ongithub.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
case "$TAG" in v[0-9]*)matches across newlines (a multiline input injects extra$GITHUB_OUTPUTlines) and accepts non-tag refs. Anchor it to exact semver, reject CR/LF, andgh release view "$TAG"before checkout.npm install -g npm@^11.5.1floats to the latest 11.x in the publish job; pin an exact version and assertnpm --versionsatisfies it.