GitHub Actions is the weakest link (14 minute read)
GitHub Actions has become the primary attack vector for open source supply chain compromises, with almost every major incident in the past 18 months exploiting features working exactly as designed.
What: An analysis of how GitHub Actions' design choices—mutable version tags, dangerous default permissions, and triggers that grant secrets to untrusted code—have enabled a wave of supply chain attacks compromising packages on PyPI, npm, and other registries.
Why it matters: The adoption of OIDC-based trusted publishing has concentrated package registry security on GitHub Actions, meaning a CI workflow vulnerability now has the same impact as compromising a maintainer's publishing credentials, but the platform treats public repos building open source packages the same as private enterprise CI.
Takeaway: Run zizmor-action on your workflows to catch dangerous patterns, pin actions to SHA commits instead of tags, and set permissions: {} at the top of every workflow file to avoid default write access.
Deep dive
- The pull_request_target trigger runs in the base repo's context with full secret access and write tokens, but can execute code from untrusted forks—combining it with fork checkouts handed attackers credentials in spotbugs, Ultralytics, nx, prt-scan, and Trivy incidents
- Action version references are mutable git tags in external repos that can be force-pushed by anyone with write access, demonstrated when tj-actions compromise affected 23,000 downstream repos through tag hijacking
- GitHub's runner resolves action references against the entire fork network object pool, meaning SHA commits that exist only in attacker forks and never reached upstream branches execute as if maintainers approved them
- Cache poisoning crosses trust boundaries silently with no UI indication that an entry was written by an untrusted job, used in Ultralytics attack where poisoned cache from fork PR later executed during legitimate release workflow
- Template expansion with $ syntax performs textual substitution before the shell sees the script, turning PR titles and issue comments into executable code when interpolated into run: steps, exploited in nx and elementary-data attacks
- The elementary-data incident went from GitHub comment to malicious PyPI package in 10 minutes through issue_comment trigger with default write token, using no PR approval or maintainer interaction
- GITHUB_TOKEN defaults to write permissions on repos created before February 2023, and workflows get this by default unless explicitly setting permissions: block
- The nx/s1ngularity attack injected commands through PR titles that harvested AI coding assistant credentials, using them to enumerate and exfiltrate over 5,000 private repositories
- PyPI, npm, RubyGems and crates.io have adopted GitHub Actions OIDC as their primary publishing mechanism, concentrating trust that was previously distributed across thousands of maintainer credentials onto one CI platform
- Statistics show 91% of PyPI packages using third-party actions reference at least one by mutable tag, and two-thirds have no permissions block on at least one workflow
- GitHub's roadmap includes workflow lockfiles, policy controls, scoped secrets and egress firewalls, but everything is opt-in and months away, with no plans to change defaults due to breaking existing workflows
- The author argues public repos building packages for millions of downstream users warrant different risk calculus than private enterprise CI, justifying breaking changes that would prevent attacks
- zizmor audit tool catches most of these patterns (dangerous-triggers, cache-poisoning, unpinned-uses, template-injection, excessive-permissions) and flagged elementary-data three weeks before compromise
- Suggested breaking changes include read-only tokens for all public repos, refusing to expand github.event inside run steps, refusing cache restores in pull_request_target jobs, and requiring immutable references for workflows requesting id-token: write
- The prt-scan campaign automated the attack pattern, opening hundreds of PRs with plausible-looking generated diffs across repositories with pull_request_target misconfigurations over six weeks
Decoder
- pull_request_target: A GitHub Actions workflow trigger that runs in the context of the base repository (not the fork) with access to secrets and write tokens, designed for workflows that label or process PRs from forks
- OIDC trusted publishing: Authentication method where package registries verify the identity of the publishing CI system through OpenID Connect tokens rather than requiring long-lived API credentials stored as secrets
- id-token: write: A GitHub Actions permission that allows a workflow to request OIDC tokens, which package registries use to verify the workflow is authorized to publish packages
- Mutable tags: Git tags that can be moved to point at different commits, unlike SHA commit hashes which are immutable references to specific code snapshots
- Cache poisoning: Attack where untrusted code writes malicious content into a shared cache that is later restored and executed by a trusted workflow
- Template expansion: GitHub Actions' $ syntax that substitutes values into strings before passing them to the shell, without automatic escaping or quoting
- GITHUB_TOKEN: An automatically-generated authentication token that GitHub provides to workflows, with permissions defaulting to write access on older repositories
- Imposter commits: Commits that exist in a fork's object store but never reached any branch in the upstream repository, yet are executable through the parent repo's namespace
- zizmor: Third-party security linter for GitHub Actions workflows that detects common dangerous patterns and misconfigurations
Original article
Almost every open source supply chain incident from the past eighteen months involves GitHub Actions features behaving exactly as documented. Actions is basically a package manager with no lockfile, no integrity hashes, and no transitive visibility. The whole product is a collection of features that are convenient, but very easy to assemble into something dangerous. GitHub plans to add fixes, but the company says that changing the defaults will break existing workflows.