2026-10-14 · tool deep dive · 4 min
A Version Tag Leaked CI Secrets: Lessons From CVE-2025-30066 for Your GitHub Actions
On March 14 and 15, 2025, the version tags of a popular GitHub Action stopped meaning what everyone thought they meant. A threat actor modified the tags v1 through v45.0.7 of tj-actions/changed-files so they pointed at…

This article was written with AI assistance and reviewed by the author.
On March 14 and 15, 2025, the version tags of a popular GitHub Action stopped meaning what everyone thought they meant. A threat actor modified the tags v1 through v45.0.7 of tj-actions/changed-files so they pointed at a single malicious commit, 0e58ed8. The result, as recorded in the National Vulnerability Database, was that remote attackers could discover secrets by reading Actions logs [1]. NVD rates it 8.6 (High) and tracks it as CVE-2025-30066 [1].
Nobody had to change a line of their own workflow to be affected. If a pipeline said uses: tj-actions/changed-files@v45, the next run quietly fetched the attacker's code.
Why a tag is not a version
A git tag feels like a release number, but it is just a movable pointer. Whoever controls the upstream repository can move it. That is exactly what happened here [1].
CISA's alert on the incident asked organizations to audit every project that used any version of tj-actions/changed-files between 2025-03-12 and 2025-03-15, and for public repositories that ran the malicious commit, to check for exposed access keys, GitHub Personal Access Tokens, npm tokens and private RSA keys [2]. In other words: assume anything the workflow could see was compromised.

GitHub's own guidance has long pointed at the fix. Its secure-use reference recommends pinning actions to a full-length commit SHA, because an attacker would then need a SHA-1 collision for a valid Git object to slip in a backdoor [3]. The same page notes that workflow dependency lockfiles now supersede SHA pinning for repositories that adopt them [3], so check which option fits your setup.
The quieter cousins of the same mistake
A retagged action is the dramatic case. GitHub Actions has a few other well-documented ways for an outsider to end up running code with your secrets:
- Pwn requests. GitHub Security Lab warns that combining the
pull_request_targettrigger with an explicit checkout of an untrusted pull request is a dangerous practice that may lead to repository compromise [4]. - Script injection. Fields such as
github.event.pull_request.titleare attacker controlled. The recommended fix is to pass them through an intermediate environment variable instead of interpolating${{ }}straight into arun:script [5]. - Over-privileged tokens. GitHub advises granting the
GITHUB_TOKENthe minimum required permissions, with read-only access to repository contents as a good default [3].
Each of these is easy to spot in code review once you know the pattern, and easy to miss in a 200-line YAML file at 6pm on a Friday.
Letting a tool do the staring
workflow-sentinel is a small CLI that scans .github/workflows/*.yml and action.yml files and fails the build when it finds these patterns. To see it work, here is a deliberately bad workflow that combines all of the problems above:
name: PR checks
on: pull_request_target
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: tj-actions/changed-files@v45
id: changed
- run: echo "Checking ${{ github.event.pull_request.title }}"
- run: npm ci && npm test
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Running npx workflow-sentinel (version 1.1.0) in that repository produces this real output:

Seven findings, each tied to a line number and a concrete fix: two CRITICAL (the pull-request-head checkout under pull_request_target, and the title interpolated into run:), three HIGH (two mutable tags, plus secrets exposed to a job running PR code) and two MEDIUM (no permissions: block, and checkout without persist-credentials: false). The exit code is non-zero because the default --fail-on level is high.
For contrast, the same command against the current workflows in the expressjs/express repository reported 0 findings across 5 files and passed.
Fixing it, not just flagging it
The part that makes this practical on a real codebase is --fix. It pins actions to commit SHAs using the GitHub API (with GH_TOKEN or GITHUB_TOKEN), and --fix-dry-run shows the changes first. A few other options:
--trusted-owner actionsskips SHA-pin requirements for owners you choose to trust.--sarif <file>emits SARIF 2.1.0 for GitHub code scanning.--jsonand--markdownfor reports and PR comments.
A minimal CI step:
- name: Harden workflows
run: npx workflow-sentinel@1 --sarif workflow-sentinel.sarifThe takeaway
CVE-2025-30066 did not exploit a bug in anyone's application. It exploited a habit: trusting that @v45 means the same code tomorrow as it did yesterday [1]. Pin what you run, give the token as little as possible, and keep attacker-controlled strings out of shell scripts [3][4][5]. Then let a linter check it on every push, because the YAML will keep changing.
- Site: https://mrlouiswright.github.io/workflow-sentinel/
- npm: https://www.npmjs.com/package/workflow-sentinel
- GitHub: https://github.com/MrLouisWright/workflow-sentinel
References
- NIST National Vulnerability Database. "CVE-2025-30066 Detail". 2025. https://nvd.nist.gov/vuln/detail/CVE-2025-30066
- CISA. "Supply Chain Compromise of Third-Party tj-actions/changed-files (CVE-2025-30066) and reviewdog/action-setup@v1 (CVE-2025-30154)". 2025. https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction
- GitHub Docs. "Secure use reference". Accessed Oct 2026. https://docs.github.com/en/actions/reference/security/secure-use
- GitHub Security Lab (jarlob). "Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests". 2021. https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
- GitHub Security Lab (jarlob). "Keeping your GitHub Actions and workflows secure Part 2: Untrusted input". 2021. https://securitylab.github.com/resources/github-actions-untrusted-input/
Research notes, monthly.
One email a month on malware analysis and security tooling.
One email a month. No tracking pixels. Unsubscribe any time.