Skip to content

security: pin GitHub Actions to commit SHAs instead of mutable tags (repo-wide) #1099

Open

Description

@robertsLando

Summary

All GitHub Actions across .github/ are referenced by mutable version tags (@v4, @v9, …) rather than pinned to immutable commit SHAs. Tags are movable refs: the action's owner — or anyone who compromises the action repo — can re-point a tag to different code, which then runs in our CI on the next workflow run with no PR, no diff, and no notification. SHA pins (@<40-char-sha> # vN) are immutable: the only way our run changes is a reviewable commit that bumps the SHA.

This was surfaced while reviewing the new mqtt-compat.yml workflow, but the exposure is repo-wide, so tracking it here rather than scoping the fix to one file.

Why it matters

  • Supply-chain substitution. A compromised action with a moved tag executes immediately in every downstream workflow. Real precedent: tj-actions/changed-files (CVE-2025-30066, March 2025) had its version tags retroactively moved to secret-exfiltrating code, exposing ~23k repos; reviewdog actions were hit the same way. SHA-pinned consumers were unaffected.
  • Blast radius is amplified by token scope. Some of our workflows run with write permissions:
    • labeler.yml — pull-requests: write
    • codeql.yml — security-events: write, runs analysis actions against the base branch
    • mqtt-compat.yml / sticky-pr-comment — pull-requests: write (the github-script step holds the token while it runs)
      A compromised action there is attacker-controlled code holding a write token, not just wrong results.
  • Third-party > first-party risk. codecov/codecov-action is third-party — exactly the higher-risk category (tj-actions, codecov-bash were all third-party).
  • Reproducibility. Floating major tags (@v4 → latest v4.x.y) drift under us: the same workflow file can behave differently over time with no change on our side.

Current state — every ref is a bare tag (nothing SHA-pinned)

File Action refs
.github/workflows/ci.yml actions/checkout@v4, actions/setup-node@v4, codecov/codecov-action@v5
.github/workflows/codeql.yml actions/checkout@v4, github/codeql-action/init@v3, github/codeql-action/analyze@v3, actions/dependency-review-action@v4
.github/workflows/labeler.yml actions/labeler@v5
.github/workflows/benchmark-compare-serial.yml actions/checkout@v4, actions/setup-node@v4
.github/workflows/mqtt-compat.yml actions/checkout@v7, actions/setup-node@v6, actions/setup-python@v6, actions/upload-artifact@v7
.github/actions/sticky-pr-comment/action.yml actions/github-script@v9

.github/dependabot.yml already enables the github-actions ecosystem — but Dependabot only updates the pinning style already in use. On bare tags it bumps the tag; it never converts tags to SHAs and does nothing about a moved/floating tag. The immutability benefit only kicks in once refs are SHA-pinned, after which Dependabot maintains the SHA and the # vN comment on each release.

Proposed fix

Pin all action refs across .github/ to full commit SHAs with a trailing # vN comment, in one consistent sweep:

- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

Dependabot (already configured) then keeps both the SHA and the comment current, with every update arriving as a reviewable PR instead of a silent tag move.

Doing this piecemeal (only mqtt-compat.yml) would leave the higher-risk codeql / codecov / labeler workflows on bare tags — worse than uniform tags — so the fix should cover everything at once.

References

Surfaced during review of #1094.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions