Skip to content

Guard against corrupted action pins in generated workflows - #10266

Merged
Evangelink merged 3 commits into
mainfrom
dev/amauryleve/guard-corrupted-action-pins
Jul 28, 2026
Merged

Guard against corrupted action pins in generated workflows#10266
Evangelink merged 3 commits into
mainfrom
dev/amauryleve/guard-corrupted-action-pins

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes #10258.

Problem

A locally installed gh aw extension silently rewrites security-relevant action pins in every .lock.yml it touches — actions/checkout downgraded v7.0.1 → v7.0.0, and github/gh-aw-actions/setup un-pinned from an immutable SHA to a mutable @v0.83.1 tag. The compiler_version header 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 every uses: 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 the gh aw CLI, a token, or network access in the gate).

Three rules:

Rule Scope Catches
R1 — every uses: must be pinned to a 40-char commit SHA generated files (*.lock.yml + gh-aw-generated .yml) the github/gh-aw-actions/setup@v0.83.1 un-pinning
R2 — SHA-pinned references must match .github/aw/actions-lock.json, including the trailing # vX.Y.Z label all files the actions/checkout v7.0.1 → v7.0.0 downgrade
R3 — an action must resolve to a single SHA repo-wide all files drift for actions the compiler injects but that are absent from actions-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 from actions-lock.json at compile time rather than an executable reference, and self-heals on the next recompile.

Also documents the hazard in .github/workflows/README.md and .github/copilot-instructions.md (mitigation 3): compile on the pinned toolchain, always re-read the .lock.yml diff, and run the script before pushing.

Pre-existing problems the guard found

Both fixed in this PR:

  1. .github/aw/actions-lock.json pinned an annotated tag object, not a commit. github/codeql-action/upload-sarif@v4.37.3 recorded c54b30b7…, which is the tag object SHA; it dereferences to commit e4fba868… — the SHA the compiled workflow actually uses. GitHub Actions resolves uses: to a commit, so the lock file recorded a value that cannot be used as a pin. Verified against the GitHub API:

    github/codeql-action@v4.37.3: ref -> tag c54b30b7…  ->  commit e4fba868…
    

    (Contrast actions/github-script@v9.0.0, where actions-lock.json correctly records the dereferenced commit.)

  2. backport-base.yml labelled ten actions/github-script pins # v7 while pinning the v9.0.0 commit — a stale label left over from an upgrade.

Verification

Action pin audit passed: 2076 reference(s) across 45 file(s).

Both corruptions from the issue were injected into add-tests.lock.yml and confirmed to fail the audit:

- add-tests.lock.yml:110: `github/gh-aw-actions/setup@v0.83.1` is not pinned to a commit SHA. …
- add-tests.lock.yml:214: `actions/checkout` is pinned to 9c091bb2… (v7.0.0) but
  .github/aw/actions-lock.json records 3d3c42e5… (v7.0.1).

R3 was verified separately by perturbing an actions/setup-node pin (an action absent from actions-lock.json). All probes were reverted; markdownlint-cli2 and YAML/JSON parsing pass.

No .lock.yml file 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 against github/gh-aw.

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
Copilot AI review requested due to automatic review settings July 27, 2026 16:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread .github/scripts/check_action_pins.py Outdated
Comment thread .github/scripts/check_action_pins.py Outdated
@Evangelink
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
Copilot AI review requested due to automatic review settings July 27, 2026 17:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread .github/scripts/check_action_pins.py Outdated
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
Copilot AI review requested due to automatic review settings July 27, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@Evangelink Evangelink added the state/needs-review Awaiting review from the team. label Jul 27, 2026
@Evangelink
Evangelink merged commit 68c9411 into main Jul 28, 2026
22 checks passed
@Evangelink
Evangelink deleted the dev/amauryleve/guard-corrupted-action-pins branch July 28, 2026 07:34
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-review Awaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Local gh aw compile silently corrupts two pinned actions in generated .lock.yml files

3 participants