Skip to content

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

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.ymlpull-requests: write
    • codeql.ymlsecurity-events: write, runs analysis actions against the base branch
    • mqtt-compat.yml / sticky-pr-commentpull-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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions