Skip to content
9 changes: 6 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,20 @@ jobs:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: pnpm install

# pnpm@10 delegates `pnpm publish` to the npm CLI; OIDC trusted publishing
# requires npm >=11.5.1, which Node 24's bundled npm only satisfies from
# ~24.6 onward. Install a recent-enough npm so we don't depend on which Node patch resolves.
- name: Ensure npm CLI supports OIDC trusted publishing
run: npm install -g npm@11.5.1

Comment on lines +70 to +75
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing: actions/checkout@v6 and actions/setup-node@v6 use floating semver tags in the publish job while pnpm/action-setup and changesets/action in the same job are pinned to full commit SHAs. Consider pinning these to commit SHAs for consistency, though this PR actually improves the overall risk profile by replacing a long-lived NPM_TOKEN with a short-lived OIDC token.

Extended reasoning...

In the publish job (which holds id-token: write and runs in the release environment), actions/checkout@v6 and actions/setup-node@v6 use floating semver tags, while pnpm/action-setup and changesets/action in the same job are pinned to full commit SHAs. This inconsistency is real and worth noting.

This inconsistency is entirely pre-existing — the floating @v6 tags predate this PR. The PR only adds an npm install step and removes NPM_TOKEN/NODE_AUTH_TOKEN env vars.

The refutation makes a compelling point that must be addressed: this PR actually improves the exfiltration risk profile. Before this PR, a compromised floating action in the publish job could exfiltrate the long-lived NPM_TOKEN secret (valid indefinitely, usable from anywhere). After this PR, the worst-case exfiltration is a short-lived OIDC token that expires quickly and can only be exchanged from within a trusted GitHub Actions context. Short-lived OIDC tokens are strictly less dangerous than long-lived static secrets — so the PR is a net improvement from a security standpoint.

Additionally, id-token: write was already present in the publish job before this PR (added in #1836), so the OIDC token was already exposed to these floating actions. This PR changes nothing about that existing exposure.

Mitigating factors: actions/checkout and actions/setup-node are official first-party GitHub-maintained actions. Compromising their v6 tag would require compromising GitHub's own infrastructure — a substantially higher bar than a third-party action. Many organizations deliberately exempt first-party GitHub actions from SHA-pinning requirements for this reason.

Step-by-step proof of inconsistency: (1) Attacker somehow moves the v6 tag on actions/checkout to a malicious commit. (2) The publish job runs and executes the malicious checkout action. (3) The compromised action can request a GitHub OIDC token via id-token: write. (4) However, the token is short-lived (~15 minutes) and the attacker must use it within that window. Compare this to the pre-PR state where the long-lived NPM_TOKEN was in the environment and would have been trivially exfiltrable to any external service.

Fix: Pin actions/checkout and actions/setup-node to their full commit SHAs, consistent with the other actions in the same job. Low-effort improvement for consistency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repo convention is to SHA-pin third-party actions and float first-party actions/* (see main.yml and the other workflows). This PR doesn't touch those lines. SHA-pinning first-party GitHub actions would be a separate repo-wide change.

- name: Publish to npm
uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1
with:
publish: pnpm run ci:publish
env:
GITHUB_TOKEN: ${{ github.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: 'true'
Loading