· 6 min read ·

SHA Pinning Checks Integrity, Not Trustworthiness

Source: lobsters

When the GitHub Actions ecosystem had its supply chain moment in March 2025, a lot of teams checked their workflows and felt reassured. They were pinning to full commit SHAs, so they were safe. Some of them were right. A meaningful number were not.

The tj-actions/changed-files incident (CVE-2025-30066) is the clearest recent illustration of what SHA pinning does and does not buy you. Understanding the boundary between those two things is more useful than any specific remediation advice.

What SHA Pinning Does

In a GitHub Actions workflow, you reference external actions with a uses: field. There are three styles:

# Tag reference — mutable, can be force-pushed at any time
- uses: actions/checkout@v4

# Branch reference — always latest HEAD, maximally mutable
- uses: actions/checkout@main

# SHA reference — immutable, content-addressed
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

The SHA version is a 40-character hex string representing the git commit object. Git computes it as a cryptographic hash of the commit’s contents: the source tree, parent commits, author metadata, and message. You cannot modify any part of a commit without changing its SHA. The reference is a precise, unforgeable pointer to a specific snapshot of the action’s code.

The security benefit is direct: if an attacker compromises a maintainer’s account and force-pushes a malicious commit under the v4 tag, your workflow continues running the commit you audited. Tag mutations are among the most common attack vectors against GitHub Actions, and SHA pinning defeats them entirely.

This is a genuine and useful property. OpenSSF Scorecard grades projects on it; tools like StepSecurity’s Harden-Runner exist to help teams adopt it; GitHub’s own security guidance has recommended it for years. The place SHA pinning falls short is in the security guarantee many teams assume it delivers.

The tj-actions Incident

On March 14, 2025, the tj-actions/changed-files action was compromised by an attacker who had first compromised reviewdog/action-setup (CVE-2025-30154). The reviewdog action was used inside tj-actions/changed-files’s own CI pipeline. By controlling what that pipeline executed, the attacker gained sufficient access to modify the action’s code and force-push all version tags, from v1 through v45, to a malicious commit.

The malicious payload iterated over environment variables in the GitHub Actions runner and printed them to the workflow log. In public repositories, workflow logs are publicly readable, meaning any secrets present in the environment — cloud credentials, npm tokens, GITHUB_TOKEN — were exposed to anyone monitoring the logs. An estimated 23,000 repositories running tag-based references were immediately affected.

Repositories with SHA pins pointing to pre-compromise commits were protected. Their workflows continued running the audited code. Many of those SHA-pinned repositories, though, were also running Dependabot or Renovate to keep their pins current. When a new version of an action is released, these tools open a pull request that updates the SHA from one 40-character hash to the next. Repositories that had recently merged such a PR were running the compromised commit, pinned to a specific SHA and still fully exposed.

The Automation Paradox

SHA pinning without automated updates creates a real problem: dependencies go stale and legitimate security patches in upstream actions never reach you. SHA pinning with automated updates restores the attack surface that pinning was meant to close, unless those update PRs receive scrutiny equivalent to any other code change.

In practice, Dependabot PRs get rubber-stamped. The diff shows a commit SHA changing from one 40-character string to another, with a note indicating a minor version bump. Developers treat these as administrative noise. The implicit assumption is that the upstream maintainer is trustworthy and that a point release contains only legitimate changes. The tj-actions attack was not a subtle backdoor buried in a diff; it was obvious to anyone who looked at it. Almost nobody looks at Dependabot diffs for GitHub Actions.

Under this common workflow, the security value of SHA pinning reduces to a timing guarantee: you are protected if the attack occurred after your last pin update. That is meaningfully better than tag-based references, but considerably weaker than the claim of “you are protected by SHA pinning.”

What XZ Utils Adds to the Picture

The XZ Utils backdoor (CVE-2024-3094), discovered in April 2024, demonstrates a deeper failure mode. A contributor operating under the pseudonym “Jia Tan” spent approximately two years building trust in the XZ Utils project, contributing legitimate improvements and eventually gaining co-maintainer status. Once trusted, they introduced a carefully obfuscated backdoor into the build system’s autoconf macros, targeting SSH authentication on specific systemd-based Linux distributions.

SHA pinning does not address this attack at all. The malicious tarballs had correct checksums; distribution package metadata accurately reflected the SHA-256 hashes of the backdoored releases. Anyone pinning to version 5.6.0 or 5.6.1 was pinning to compromised software, and hash verification would not surface that fact.

The issue was not integrity. The content was precisely what the maintainer produced. The issue was trust: the maintainer, or someone presenting convincingly as a trustworthy maintainer over years of interaction, had malicious intent. SHA pinning proves you received exactly what the distribution intended to give you. It says nothing about whether that distribution should have been trusted.

Integrity and Trust Are Different Properties

These two properties are distinct and SHA pinning only addresses one of them:

Integrity: The artifact you are consuming is identical to the artifact at the reference you specified. No one tampered with it in transit or at rest.

Trust: The artifact at that reference was produced by parties with good intent, through a process you have reason to believe generated only what was intended.

SHA pinning handles integrity. Nothing in the standard SHA pinning workflow addresses trust. This is not a flaw in SHA pinning; it is a precise description of its scope. The problem arises when teams treat a 10/10 on the OpenSSF Scorecard Pinned-Dependencies check as evidence of supply chain security rather than evidence of one specific integrity property.

The attacker’s playbook makes this concrete. SHA pinning defeats tag mutation, CDN content substitution, and account takeovers where you have frozen your pin and refuse to update. It does not defeat a malicious payload that was already in the commit you pinned, a compromised build system that produces different binaries from the same source, or a patient insider who earns maintainer trust over years and then ships a backdoored release.

What Actually Addresses the Gap

Sigstore and SLSA (Supply chain Levels for Software Artifacts) operate at the layer SHA pinning cannot reach.

Sigstore’s cosign tool enables signing artifacts using OIDC identity, where the identity is cryptographically tied to a specific GitHub Actions workflow in a specific repository. An artifact signed with Sigstore carries the claim: “This was built by workflow build.yml in repository actions/checkout, running on commit abc123, in an ephemeral runner.” This is not merely “this content matches this hash”; it is “this content was produced by this verifiable process.” All Sigstore signatures are recorded in Rekor, an append-only transparency log that enables after-the-fact auditing.

GitHub introduced artifact attestations in 2024, built on SLSA provenance and Sigstore. Consumers can verify with gh attestation verify, confirming that an artifact traces back to a specific source commit and workflow. SLSA Level 3 adds hardened, ephemeral build environments; Level 4, aspirationally, requires reproducible builds, meaning independent parties can rebuild an artifact from source and confirm it matches the published binary. Reproducible builds would make XZ-style build system manipulation detectable, because an independently built artifact would differ from the compromised one.

None of these mechanisms solve the long-con insider threat that XZ Utils demonstrated. But they do raise the bar considerably. A Sigstore-verified artifact requires the attacker to compromise an account with rights to trigger the production workflow, not merely push to a tag.

Where SHA Pinning Fits

SHA pinning belongs in your workflow. It prevents a real class of attacks, particularly tag mutation attacks, which are among the most opportunistic. Combined with meaningful review of dependency update PRs, it provides genuine protection at low cost.

What it does not provide is an answer to the trust question. As the source article on this topic puts it, the comfort of a SHA pin comes partly from the feeling of having done something technically rigorous, while the harder work, evaluating whether the code being pinned is trustworthy, remains untouched.

A complete supply chain posture requires layers: SHA pinning for referential integrity, Sigstore attestations for provenance, and review practices that treat dependency updates as the trust decisions they are. Most teams have adopted the first layer and consider the problem addressed. The tj-actions incident cost many of them that assumption.

Was this interesting?