Repository navigation
fix(ci): stop handing the checkout token to untrusted workflow code - #2420
Merged
Merged
Conversation
`actions/checkout` writes the job's GITHUB_TOKEN into `.git/config` as `http.<host>.extraheader` unless `persist-credentials: false` is set. Only `ci.yml` set it, so four pull-request-triggered workflows (`js.yml`, `python.yml`, `coverage.yml`, `site.yml`) handed a live repository credential to fork-authored code running in the same job — dependency build scripts, npm lifecycle scripts, pytest, `pnpm build` — and the release-time build/test jobs of the three publish workflows did the same for artifact builds. Every checkout that does not back an authenticated `git`/`gh` call now opts out. The credential survives only in the eight tag-verification, source-verification and release jobs that push or fetch with it, all of which run from main-reachable refs and execute no PR-authored code. Also: `release.yml` declares a top-level `permissions: contents: read` instead of inheriting the repository default, and `cli-binaries.yml` registers the `::add-mask::` for the Homebrew PAT before exporting it rather than after. The rule is derived rather than allowlisted, so a new workflow cannot reintroduce the gap: `test_ci_supply_chain.py` fails any job that persists the token without running `git`/`gh`, any job at all in a pull-request-triggered workflow that persists it, any workflow without a read-only default permissions block, any secret value routed through `$GITHUB_ENV`, and any Doppler-fetched value that travels before it is masked. Recorded as TM-INF-034; maintenance passes now audit workflow definitions, not just run results.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
bashkit | 3ad0eda | Commit Preview URL Branch Preview URL |
Sep 18 2026, 05:16 AM |
The canonical ledger and `crates/bashkit/docs/threat-model.md` must list the same threat IDs; `public_threat_model_doc_covers_every_spec_threat_id` caught the new entry missing from the user-facing table.
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
CI jobs no longer hand a live repository credential to the code they execute.
actions/checkoutwrites the job'sGITHUB_TOKENinto.git/configashttp.<host>.extraheaderunlesspersist-credentials: falseis set. Onlyci.ymlset it. Four pull-request-triggered workflows therefore ran fork-authored code
with a usable repository token sitting on disk in the same job:
js.ymlbuild.rs, node examplespython.ymlcargo,pytest,maturincoverage.ymlcargo llvm-covsite.ymlpnpm buildThe release-time build/test jobs of
publish-js.yml,publish-python.ymlandpublish-wasm.ymldid the same for artifact builds, as didnightly.ymlandfuzz.yml.Every checkout that does not back an authenticated
git/ghcall now opts out.The credential survives in exactly eight jobs — tag verification, source
verification, and release — all of which push or fetch with it, run from
main-reachable refs, and execute no PR-authored code.
Two smaller fixes in the same area:
release.ymlwas the only workflow inheriting the repository's defaultpermissions. It now declares
permissions: contents: readat the top level, sothe job's
contents: write+actions: writestay opt-in and a job added laterstarts read-only.
cli-binaries.ymlregistered::add-mask::for the Homebrew PAT afterexporting it. Masking now happens first, so a
set -xor error trace in betweencannot print it.
Why
A prompt to check a GitHub Actions run for token leakage turned into an audit of
bashkit's own workflows. No secret value is leaked — the Doppler fetch pattern is
sound (
fetch → ::add-mask:: → $GITHUB_OUTPUT, never$GITHUB_ENV) and there are nopull_request_targettriggers, env dumps, or untrusted${{ github.event.* }}interpolation into
run:blocks.What the audit did find is that TM-INF-026's documented mitigation — "checkout
credentials are not persisted" — was enforced for
ci.ymlonly, by a test thathard-coded that filename. Every other workflow had drifted past it unnoticed. The
exposed token is read-only on a public repo, so impact is bounded, but it is a live
credential readable by any
build.rsor npm lifecycle script in a fork PR.Recorded as TM-INF-034 in the threat model.
Before / After
No change to any workflow's behavior — this is a credential-surface change. The
observable difference is in the invariant the test suite enforces.
Before — the rule existed for one file, by name:
After — the rule is derived from what each job actually does, so no allowlist
has to be maintained and a new workflow cannot reintroduce the gap. Four invariants,
each verified to fail on a deliberately broken workflow:
Full suite green on the real tree:
The second commit is that suite doing its job:
TM-INF-034was in the canonicalledger but not yet in
crates/bashkit/docs/threat-model.md, andpublic_threat_model_doc_covers_every_spec_threat_idcaught it.Maintenance passes now audit workflow definitions rather than only run results —
a new Workflow Credential Audit checklist in
knowledge/operations/maintenance.md,referenced from the
maintainskill.Risk
it. Each of the 23 opted-out checkouts was checked for
git/ghusage in its jobbefore the flag was added, and the derived test asserts that property continuously.
A failure would surface immediately as an auth error on this PR's own run — all 46
checks are green.
Checklist