Do Cao Hieu
Do Cao Hieu
Do Cao Hieu
Do Cao Hieu
Do Cao Hieu

DevOps Engineer

Site Reliability Engineer

Platform Engineer

Auditing an old repo after the trivy-action tag hijack

October 26, 2026supply-chain · github-actions · security · ci-cd · audit

While cleaning up my public GitHub repos I found a workflow that referenced a compromised trivy-action tag, checked whether I was exposed, and replaced the tag with a pinned commit and a self-test.

Context

On 26 October 2026 I was preparing my public GitHub profile for job applications: making repos public, fixing READMEs, deleting forks. One of the pinned repos was reusable-workflows, a set of GitHub Actions workflows and composite actions I wrote in March 2026 to detect a project's language, build, test, scan and deploy it.

It contains a security job that runs aquasecurity/trivy-action. That action was one of the targets of a supply-chain attack on 19 March 2026.

What I saw

The workflow file had this line:

uses: aquasecurity/trivy-action@0.28.0

I knew about the attack in general terms. Seeing my own repo reference a tag from the affected range was the prompt to stop tidying and check.

Finding the cause

Facts first, from the GitHub advisory (GHSA-69fq-xp46-6x23) and the NVD entry for CVE-2026-33634:

  • On 19 March 2026 an attacker, using credentials left over from an earlier incident, force-pushed 76 of the 77 version tags of trivy-action to commits containing an infostealer.
  • The injected code ran before the real scan. It read the runner's process memory and many credential paths, encrypted the result and sent it out. As a fallback it created a public repo named tpcp-docs on the victim's account.
  • The advisory's exposure window for trivy-action starts at about 17:43 UTC on 19 March and ends at about 05:40 UTC on 20 March.
  • Tags 0.0.1 to 0.34.2 were affected, so 0.28.0 was one of them. They were later deleted. Only 0.35.0 was safe, because it was an immutable release.

Then my own exposure, checked with the gh CLI, read-only:

  • Runs. The repo has had two workflow runs. The only one from March started at 15:31 UTC on 19 March and failed with zero jobs, so no step ever executed. That start time is about two hours before the earliest suspicious activity in the advisory.
  • Secrets. The repo has no Actions secrets (the API reports 0), so there was nothing to steal even if a job had run.
  • Callers. No repository of mine called the workflow. It is a workflow_call workflow and nothing used it.
  • Exfiltration artifact. My account has no tpcp-docs repository.

I also checked the history, and it surprised me. When the attack happened, the file referenced aquasecurity/trivy-action@master, a moving branch. The 0.28.0 pin only arrived in a commit on 11 June, whose message calls it a fix: "unpinned third-party action running with secrets: inherit is supply-chain risk". That "pin" was a tag, which is mutable, and by June the tag had been deleted upstream. A lookup of 0.28.0 on the upstream repo now returns not found. So the June fix looked like hardening but referenced something that no longer existed.

Root cause

I trusted a version tag to mean a fixed version. Git tags are pointers that anyone with write access can move, and the attacker had write access. A tag gives you a name, not a guarantee about the code.

The repo was also abandoned: nobody was watching it, so the advisory never reached me.

The fix

I pinned the action to a full commit SHA, with a comment recording why:

uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0

My first pin was wrong. I took the SHA from git/ref/tags/v0.36.0, and for an annotated tag that is the SHA of the tag object, not of the commit it points to. It looks exactly like a commit SHA, but GitHub resolves uses: only to commits, so the scan job would have failed the first time it ran, and nothing in CI would have told me, because the self-test does not run the Trivy job. A review of the draft of this post caught it. The fix was to peel the tag:

gh api repos/aquasecurity/trivy-action/git/tags/<tag-object-sha> --jq .object.sha   # the commit
gh api repos/aquasecurity/trivy-action/commits/<sha> --jq .sha                     # must resolve

To make that mistake impossible to repeat, the self-test now has a job that finds every action pinned by SHA and fails unless the SHA is a real commit.

I also added a self-test workflow that runs on every push. It runs actionlint over all workflows, then runs the repo's own detect, build and test actions against a small Node fixture project. The first run passed.

I also added a LICENSE, rewrote the README in English, and replaced secrets: inherit in the examples with explicit secrets.

What stops it happening again

  • An action pinned by SHA, as trivy-action now is, cannot be moved by a force-push.
  • The self-test catches broken workflows on every push, so the repo is exercised rather than left alone.
  • Every SHA pin is checked to be a real commit on every push.

Limits: the self-test does not run the Trivy job itself, because that needs a caller; the pin check covers the reference, not the scan. Also, trivy-action is the only action in this repo pinned by commit SHA. Every other action is still referenced by a tag or a branch: actions/checkout@v4, gitleaks/gitleaks-action@v2, dtolnay/rust-toolchain@master and superfly/flyctl-actions/setup-flyctl@master are examples. The repo's own composite actions are called as @master too. Those references can be moved in the same way, and I have not pinned them.

What I learned

  • Tags are mutable. Pin third-party actions by commit SHA, and write the human-readable version in a comment.
  • A "pin" to a tag is not a pin. My June commit felt like a security fix. It was not one.
  • A SHA is not always a commit SHA. An annotated tag has its own object; peel it, and let CI check every pin.
  • Audit old repos. A repo you stopped using still holds credentials, workflows and references, and it will not tell you about new advisories.
  • Answer exposure questions with evidence: runs, secrets, callers, artifacts, and the attack's time window. "Probably fine" is not an answer.