Skip to content

CI: third-party actions are pinned to mutable tags, including tj-actions/changed-files #3867

Description

@akshat-kumar-singhal

Summary

Every third-party action in the workflows is referenced by a mutable tag. A tag is a pointer the upstream owner can move at any time, so the code CI executes on the next run is not necessarily the code that was reviewed when the tag was added.

.github/workflows/go.yml:128   nick-fields/retry@v4
.github/workflows/go.yml:203   nick-fields/retry@v4
.github/workflows/go.yml:332   nick-fields/retry@v4
.github/workflows/go.yml:461   golangci/golangci-lint-action@v9
.github/workflows/go.yml:471   tj-actions/changed-files@v47
.github/workflows/go.yml:557   golangci/golangci-lint-action@v9
.github/workflows/typos.yml:14 crate-ci/typos@v1.48.0
website-prod.yml:71,75         docker/login-action@v4
website-prod.yml:136           google-github-actions/auth@v3
website-stage.yml:145          google-github-actions/auth@v3

tj-actions/changed-files is the one worth acting on first: in March 2025 that action's tags were retargeted to a malicious commit that dumped runner memory — including secrets — into build logs, across every repo using it. The mechanism was precisely tag mutability; SHA-pinned consumers were unaffected. GitHub's own hardening guidance for third-party actions is to pin by full commit SHA for this reason.

Blast radius here, honestly stated

Limited but not nil. go.yml sets permissions: contents: read at the top and the code_quality job doesn't raise it, so a compromised changed-files gets a read-only GITHUB_TOKEN and no repository secrets — QLTY_TOKEN lives in upload_coverage, a different job. The realistic damage is log exfiltration of whatever is in that runner's memory and tampering with the lint result, not a repo write.

The deploy workflows are the higher-value target: website-prod.yml / website-stage.yml use docker/login-action and google-github-actions/auth in jobs that hold GOFR_WEBSITE_GOFR_DEV_DEPLOYMENT_KEY and packages: write. Those are first-party-ish (Docker, Google) and so lower-risk in practice, but they are the ones where a moved tag would actually cost something.

Suggested fix

Pin by SHA with the human-readable version in a trailing comment, which is also what #3864 just did for ls-lint/action:

uses: tj-actions/changed-files@<sha>  # v47

Then let Dependabot maintain them — .github/dependabot.yml already exists in this repo, and the github-actions ecosystem updates SHA pins and rewrites the version comment automatically, so pinning does not mean going stale. Worth confirming the github-actions ecosystem is actually enabled there as part of this.

Priority

Not urgent — nothing is known to be wrong today. Filing it because tj-actions specifically has already been the vector once, and the fix is mechanical.

Context

Found while reviewing run 31266842627.

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