Skip to content

fix(ci): pin the turbo cache action and gate e2e on writable reviewers - #315

Merged
GSTJ merged 1 commit into
mainfrom
security/pin-turbo-cache-action
Jul 29, 2026
Merged

fix(ci): pin the turbo cache action and gate e2e on writable reviewers#315
GSTJ merged 1 commit into
mainfrom
security/pin-turbo-cache-action

Conversation

@GSTJ

@GSTJ GSTJ commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Came out of auditing the repo for real vulnerabilities alongside the four Dependabot alerts in #309. The library source is clean. These two are in CI.

The turbo cache action runs next to NPM_TOKEN on a mutable tag

dtinth/setup-github-actions-caching-for-turbo@v1 is the only third-party action here that isn't actions/* or pnpm/*. It sits on a personal account and hasn't been pushed to since June 2024. @v1 is a tag that account can repoint at any time.

In release.yml it runs at step 6 of the release job. Fourteen steps later the same job writes the npm publish token to ~/.npmrc in plaintext and runs pnpm run release. It also leaves an HTTP server on localhost:41230 alive for the whole job, as the same UID. Whoever controls that tag controls what is running when the publish credentials hit disk, and this pipeline publishes to npm with no human in the loop.

In docs.yml the same action runs in a job holding pages: write and id-token: write.

Pinned both to cc723b4600e40a6b8815b65701d8614b91e2669e, which is v1.3.0 and what v1 already resolves to, so nothing about the run changes. The 21:05 docs deploy log shows the runner fetching that exact SHA.

Left alone: actions/* and pnpm/* stay on tags. Those are GitHub-owned and pnpm-owned. Pinning them means 25 more uses: lines carrying SHAs across 11 distinct actions, and a Renovate PR for every one of them, against a much smaller reduction.

Any GitHub account can unlock the 45-minute macOS job

The e2e approval gate reads the PR's reviews and takes the latest APPROVED or CHANGES_REQUESTED, with no filter on who wrote it:

--jq '[.[] | select(.state=="APPROVED" or .state=="CHANGES_REQUESTED")] | sort_by(.submitted_at) | last | .state'

This repo is public, so anyone can submit an approving review. Two throwaway accounts, one to open a fork PR and one to approve it, run 45 minutes of macos-26 on code they wrote.

Nothing leaks. I checked whether this is a pwn request and it isn't: GitHub treats pull_request_review on a fork PR the same as pull_request, so no secrets reach the runner, GITHUB_TOKEN is read-only, the job declares contents: read and references no secrets, and the caches are scoped to refs/pull/N/merge. The cost is billed compute.

Added an author_association filter for OWNER, MEMBER and COLLABORATOR. An outsider's approval now can't override the owner's CHANGES_REQUESTED, and an outsider's approval on its own leaves the gate closed.

Also quoted "$NPM_TOKEN" on the npm config set line, which clears one shellcheck SC2086.

Verification

  • actionlint over all four workflows. Four findings, all pre-existing, none on a line this touches. The SC2086 that was on the npm config set line is gone.
  • the jq filter against four hand-built review lists, covering outsider-overrides-owner, outsider-alone, collaborator, and owner
  • v1 dereferences to the pinned SHA, and the last real run fetched the same one

actionlint, the tag deref and the four gate cases

Not done here

release.yml still does npm config set //registry.npmjs.org/:_authToken "$NPM_TOKEN", which leaves the token in ~/.npmrc for the rest of the job. The env-var placeholder form npm supports would keep it out of the file, but that is the publish path and I'm not changing it in the same session as a release. The pin above removes the vector that made it reachable. Worth doing on its own, with a dry run behind it.

@GSTJ
GSTJ merged commit 429258d into main Jul 29, 2026
6 of 8 checks passed
@GSTJ
GSTJ deleted the security/pin-turbo-cache-action branch July 29, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant