· 6 min read ·

SHA Pinning and the Supply Chain Risks It Leaves Open

Source: lobsters

After the tj-actions/changed-files compromise in March 2025, SHA pinning became the canonical advice for securing GitHub Actions workflows. The recommendation appears in security guides, OpenSSF tooling, and hardening documentation from GitHub itself: pin your action dependencies to full commit SHAs rather than mutable version tags. The article “The Comforting Lie of SHA Pinning” takes aim at what that advice has become in practice, which is a checkbox that teams mark as “supply chain security done.”

The critique is valid. Understanding why requires being precise about what SHA pinning does and does not guarantee.

What a SHA Pin Guarantees

In GitHub Actions, you can reference an action by a mutable tag, a branch, or an immutable commit SHA:

# Mutable tag reference
- uses: actions/checkout@v4

# Immutable SHA pin
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

A Git commit SHA is computed over the tree object containing all file contents, the parent commit hash, and commit metadata. Changing any file or the commit message produces a different hash. When the GitHub Actions runner resolves a SHA reference, it fetches that specific commit object and verifies the content matches before executing. There is no mechanism by which a repository owner can retroactively alter what files correspond to a SHA; the hash is a cryptographic commitment to the content.

This matters because Git tags are mutable. A repository owner can run git tag -f v4 <malicious-commit> and force-push, which moves the tag to point at entirely different code. Every workflow using tag pinning will then silently start running that different code on its next execution.

The Attack SHA Pinning Was Designed to Stop

In March 2025, attackers compromised the tj-actions GitHub account, likely via a stolen personal access token. They modified tj-actions/changed-files to exfiltrate GITHUB_TOKEN and other CI secrets by printing them to publicly visible workflow logs, then moved the version tags (v35, v44, and others) to point to the malicious commit. Every repository using tag pinning immediately started running the backdoored code. Estimates placed around 23,000 repositories in the affected window. The same wave hit reviewdog/action-setup within hours by the same method.

SHA-pinned repositories were immune to this specific vector. The pinned SHA pointed to a historical commit; tag movement did not affect that reference. This is genuine, demonstrated protection, and it costs almost nothing once you configure it.

Where the Protection Stops

Tag mutation is not the only supply chain attack class, and it is not the hardest to execute.

Consider the XZ Utils backdoor, discovered in early 2024. The attacker spent roughly two years building trust as a contributor to the xz project before merging malicious code through a series of ostensibly legitimate patches. Had you SHA-pinned xz at any point after those commits landed, you would have pinned the backdoored version. The hash was correct for malicious content. SHA pinning distinguishes “this exact commit” from “whatever the tag points to today”; it says nothing about whether the commit itself is trustworthy.

Any maintainer compromise that results in a new malicious commit rather than a tag move falls into this category. If an attacker gains push access to a repository and adds backdoored code, then waits for downstream users to update their pins via Renovate or Dependabot, SHA pinning becomes the mechanism by which the malicious version propagates. The pinned SHA changed; it just changed to something you did not want.

Transitive dependencies are another gap. An action pinned at abc123... may itself call other actions using mutable tag references. Your pin locks the entry point of the outer action. What that action does inside its own code, including any uses: calls it makes, is outside your control entirely.

JavaScript-based actions present a related issue. Most GitHub Actions are either Docker-based or JavaScript-based; the JavaScript variant ships a committed node_modules directory so the action can run without installing dependencies at runtime. SHA pinning guarantees those specific pre-resolved npm packages, but those packages were resolved and bundled by whoever published the action. You are trusting that resolution, not auditing it.

The Stale Pin Trap

The practical failure mode that SHA pinning enables is the accumulation of old, unpatched action code. The full security recommendation is always to pin SHAs and use Renovate or Dependabot to keep them current. Renovate’s GitHub Actions manager detects when a SHA corresponds to an older release, opens a PR to bump it to the latest version, and includes the version tag as a human-readable comment in the updated line.

Many teams do the first half without the second. Configuring SHA pinning takes an afternoon; configuring automated updates takes another twenty minutes but feels optional once the pins are in place. A year later, those teams are running pinned but outdated action versions that may carry known CVEs. Teams using mutable tag references at least receive the maintainer’s latest patches automatically, alongside the tag mutation risk they are accepting.

SHA pinning without automated updates is worse than the baseline it replaced: it provides the security feeling without the security improvement. Configuring Dependabot for GitHub Actions is straightforward:

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Without this, or an equivalent Renovate configuration, SHA pinning degrades over time into a false sense of currency.

What a Broader Strategy Looks Like

SHA pinning is one layer. Other layers address threat classes that pinning cannot reach.

Least-privilege workflow permissions are the highest-leverage control that many repositories skip. The default GITHUB_TOKEN in GitHub Actions has broad write access that most workflows do not use. Scoping permissions explicitly limits what a compromised action can do with the token:

permissions:
  contents: read
  pull-requests: write  # only if this workflow needs it

Minimizing the number of third-party actions is the most underrated control. Every external action is a supply chain dependency. Preferring GitHub-maintained official actions over third-party alternatives eliminates entire risk categories. When a third-party action is necessary, reviewing what it does before adding it is the same discipline that applies to any package dependency, and one that few teams apply consistently.

StepSecurity Harden-Runner monitors egress network traffic and process behavior during workflow execution at runtime. StepSecurity’s monitoring was how the tj-actions compromise was first detected publicly. Behavioral monitoring and preventive pinning operate at different points in the attack lifecycle and complement each other in ways that neither covers alone.

SLSA provenance addresses a gap that SHA pinning cannot close. SLSA provides signed attestations recording not just what commit was built but how it was built and by what build environment. GitHub’s actions/attest-build-provenance action generates SLSA provenance for artifacts produced in Actions workflows, and Sigstore’s cosign enables keyless signing of container images with an OIDC-backed transparency log. These tools shift the question from “which commit did this come from” to “can the build environment that produced this artifact be trusted,” which is a meaningfully harder bar for an attacker to clear.

A Calibrated View

SHA pinning is a correct, cheap, demonstrably effective defense against tag mutation attacks. The tj-actions incident was real, it affected tens of thousands of repositories, and SHA pinning would have prevented it.

The problem is treating SHA pinning as a supply chain security strategy rather than one layer within a strategy. Tag mutation attacks are real and SHA pinning stops them. Maintainer compromise producing new malicious commits is also real and SHA pinning does nothing to stop it. Transitive dependency attacks, insecure pull_request_target patterns, overprivileged tokens, and secrets accessible to actions all represent attack surface that SHA pinning does not reduce.

A team that SHA-pins its actions, skips automated updates, and does not address permissions or third-party action hygiene has checked a box and acquired a false confidence. A team that combines SHA pinning with automated updates, scoped permissions, minimal third-party dependencies, and behavioral monitoring has a defense posture that reflects the actual threat landscape.

The meaningful difference between those two teams is not which one learned about SHA pinning first; it is which one continued past it.

Was this interesting?