Guard against corrupted action pins in generated workflows - #10266
Merged
Conversation
A locally installed `gh aw` extension can silently rewrite security-relevant action pins in every `.lock.yml` it touches, even when its `compiler_version` header matches the CI toolchain. Observed corruptions include `actions/checkout` being downgraded (v7.0.1 -> v7.0.0) and `github/gh-aw-actions/setup` losing its immutable SHA in favour of a mutable `@v0.83.1` tag. Because the damage lands in generated files that reviewers rarely read line by line, and no CI gate detected it, a corrupted lock could ride along unnoticed in an unrelated PR. Add `.github/scripts/check_action_pins.py`, a deterministic offline audit of every `uses:` reference under `.github/workflows/` and `.github/actions/`: - generated workflows must pin every action to a 40-character commit SHA; - SHA-pinned references must match `.github/aw/actions-lock.json`, including the trailing version label; - an action must resolve to a single SHA repo-wide, which also covers actions the compiler injects but that are absent from `actions-lock.json`. `.github/workflows/check-action-pins.yml` runs it on pull requests touching workflows, composite actions, the actions lock, or the script itself. The audit found two pre-existing problems, fixed here: - `.github/aw/actions-lock.json` pinned `github/codeql-action/upload-sarif` to `c54b30b7`, the *annotated tag object* SHA of v4.37.3 rather than the commit it dereferences to (`e4fba868`). GitHub Actions resolves `uses:` to a commit, so the recorded value disagreed with the SHA the compiled workflow actually uses. - `backport-base.yml` labelled ten `actions/github-script` pins `# v7` while pinning the v9.0.0 commit. Fixes #10258 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9c114a0-a82e-40b1-910b-b27f9e7a9b0a
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an offline CI guard against corrupted GitHub Actions pins in generated workflows.
Changes:
- Audits action SHAs, version labels, and repository-wide drift.
- Adds CI enforcement and contributor guidance.
- Corrects existing CodeQL and GitHub Script pin metadata.
Show a summary per file
| File | Description |
|---|---|
.github/scripts/check_action_pins.py |
Implements the pin audit. |
.github/workflows/check-action-pins.yml |
Runs the audit in CI. |
.github/aw/actions-lock.json |
Corrects the CodeQL commit SHA. |
.github/workflows/backport-base.yml |
Corrects GitHub Script version labels. |
.github/workflows/README.md |
Documents safe compilation and auditing. |
.github/copilot-instructions.md |
Adds pin-verification guidance. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Medium
Evangelink
enabled auto-merge (squash)
July 27, 2026 17:28
Both reported in review: - `agentics-maintenance.yml` was misclassified as non-generated. Its banner reads "generated by pkg/workflow/maintenance_workflow.go" and it carries no `gh-aw-manifest` header, so header sniffing alone exempted it from R1 -- even though it holds a dozen `github/gh-aw-actions/setup*` pins, exactly the references #10258 reports being un-pinned. Classify generated files by filename (`*.lock.yml` plus the known gh-aw-generated names) and keep header banners only as a secondary net. - R2's version-label check was skipped when the label was absent, so deleting `# vX.Y.Z` evaded it. Require the label whenever the lock entry records a version and report "no version label" for the missing case. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9c114a0-a82e-40b1-910b-b27f9e7a9b0a
Reported in review. `load_actions_lock` returned an empty map when `.github/aw/actions-lock.json` was absent, which silently reduced the audit to R1 + R3. Both of those pass when every occurrence of an action is rewritten to the same stale SHA, so deleting the lock was a way to weaken the guard rather than an error. Confirmed reachable: removing the lock and downgrading `actions/checkout` repo-wide to the v7.0.0 SHA from #10258 -- the exact corruption this PR exists to catch -- passed the audit. `load_actions_lock` now raises `LockFileError` when the lock is missing, malformed, or holds no usable entries, and `main` reports it and exits 1. The `--list` mode does not consult the lock and is unaffected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9c114a0-a82e-40b1-910b-b27f9e7a9b0a
0101
approved these changes
Jul 28, 2026
Evangelink
added a commit
that referenced
this pull request
Jul 28, 2026
PR #10266 landed an equivalent audit while this branch was in review, so the two implementations collided on `.github/scripts/check_action_pins.py`. Main's version is authoritative and is kept wholesale: its rule structure (R1-R3), generated-file classification, `.github/actions/` scanning, `--list` table, and fail-closed handling of `actions-lock.json` are all preserved unchanged. Layered onto it are the evasions this branch had already found and fixed, each re-verified against a clean checkout of main inside a generated `.lock.yml` where R1 applies, using a plain unpinned reference as the control: - A quoted `"uses":` key and `uses :` with whitespace before the colon both parse as `uses` and execute, but did not match the line pattern. - `uses: >-` block scalars, `{uses: ...}` flow mappings and anchor/alias pairs execute but are invisible to any line scan. R5 now parses each workflow and reconciles per occurrence against the scan. - `docker://` was skipped entirely, so `docker://alpine:latest` passed while an equally mutable action tag failed. R4 now requires an image digest. - Owner and repository names were compared verbatim, so `Actions/Checkout` looked untracked and escaped R2 at any SHA. Comparisons are now canonical. PyYAML is a new dependency, hash-pinned in its own requirements file and installed the same way in CI; a missing import exits non-zero rather than silently degrading to a line-only scan. Also carried over: three `peter-evans/create-pull-request` references labelled `# v7` on the v8.1.1 SHA, and three mutable tags in dedup-analysis.yml. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c96caa1-8d43-4661-8baa-68a5b8699a81
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.
Fixes #10258.
Problem
A locally installed
gh awextension silently rewrites security-relevant action pins in every.lock.ymlit touches —actions/checkoutdowngraded v7.0.1 → v7.0.0, andgithub/gh-aw-actions/setupun-pinned from an immutable SHA to a mutable@v0.83.1tag. Thecompiler_versionheader does not catch it (it asserts an identity claim, not the emitted bytes), and no CI gate detected it, so a corrupted lock file could ride along unnoticed in an unrelated PR with green CI.Fix
Adds
.github/scripts/check_action_pins.py— a deterministic, offline audit of everyuses:reference under.github/workflows/and.github/actions/(mitigation 2 from the issue), wired into CI by.github/workflows/check-action-pins.yml(mitigation 1's intent, without needing thegh awCLI, a token, or network access in the gate).Three rules:
uses:must be pinned to a 40-char commit SHA*.lock.yml+ gh-aw-generated.yml)github/gh-aw-actions/setup@v0.83.1un-pinning.github/aw/actions-lock.json, including the trailing# vX.Y.Zlabelactions/checkoutv7.0.1 → v7.0.0 downgradeactions-lock.json(e.g.actions/setup-node)The generated
# Custom actions used:header block is audited alongside the live steps. The machine-readable# gh-aw-manifest:JSON header is deliberately not audited — it is compiler bookkeeping copied fromactions-lock.jsonat compile time rather than an executable reference, and self-heals on the next recompile.Also documents the hazard in
.github/workflows/README.mdand.github/copilot-instructions.md(mitigation 3): compile on the pinned toolchain, always re-read the.lock.ymldiff, and run the script before pushing.Pre-existing problems the guard found
Both fixed in this PR:
.github/aw/actions-lock.jsonpinned an annotated tag object, not a commit.github/codeql-action/upload-sarif@v4.37.3recordedc54b30b7…, which is the tag object SHA; it dereferences to commite4fba868…— the SHA the compiled workflow actually uses. GitHub Actions resolvesuses:to a commit, so the lock file recorded a value that cannot be used as a pin. Verified against the GitHub API:(Contrast
actions/github-script@v9.0.0, whereactions-lock.jsoncorrectly records the dereferenced commit.)backport-base.ymllabelled tenactions/github-scriptpins# v7while pinning the v9.0.0 commit — a stale label left over from an upgrade.Verification
Both corruptions from the issue were injected into
add-tests.lock.ymland confirmed to fail the audit:R3 was verified separately by perturbing an
actions/setup-nodepin (an action absent fromactions-lock.json). All probes were reverted;markdownlint-cli2and YAML/JSON parsing pass.No
.lock.ymlfile was hand-edited.Follow-up left open
The issue's mitigation 4 — raising the version-keyed-override key-miss behaviour upstream with
gh-aw— is not addressed here and remains worth filing againstgithub/gh-aw.