Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions skills/gha-security-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,11 @@ Does the workflow load configuration from PR-supplied files?

### Check 6: Supply Chain

Are third-party actions securely pinned?
Are **third-party** actions securely pinned to full SHAs?
- Pin third-party / external actions and reusable workflows only
- Do **not** flag first-party `actions/*` or `github/*` on version tags
- Do **not** flag same-repo / vendored (`./.github/actions/...`) as supply-chain pinning issues
- Only report when the job has secrets, OIDC, write token, release, deploy, package, or signing power — unprivileged read-only CI is not a finding

### Check 7: Permissions and Secrets

Expand All @@ -140,7 +144,9 @@ Before reporting, check if the pattern is actually safe:
| `${{ secrets.* }}` | Not an expression injection vector |
| `${{ }}` in `if:` conditions | Evaluated by Actions runtime, not shell |
| `${{ }}` in `with:` inputs | Passed as string parameters, not shell-evaluated |
| Actions pinned to full SHA | Immutable reference |
| Third-party actions pinned to full SHA | Immutable reference |
| First-party `actions/*` / `github/*` on version tags | Outside third-party pinning policy — do not flag |
| Same-repo / vendored local actions | Not third-party supply chain (review pwn-request separately) |
| `pull_request` trigger (not `_target`) | Runs in fork context with read-only token |
| Any expression in `workflow_dispatch`/`schedule`/`push` to protected branches | Requires write access — outside threat model |

Expand Down
147 changes: 101 additions & 46 deletions skills/gha-security-review/references/supply-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,77 @@

GitHub Actions workflows depend on third-party actions referenced by `uses:`. If these actions are not pinned to immutable references (full commit SHAs), attackers can compromise them via tag mutation, account takeover, or fork-and-replace attacks.

**Policy:** pin **third-party** actions and reusable workflows to a full 40-character commit SHA. Do **not** require SHA pins for first-party GitHub actions (`actions/*`, `github/*`) on version tags, or for same-repo / vendored actions.

---

## What Counts As Third-Party

| Source | Classification | Pinning finding? |
|--------|----------------|------------------|
| `actions/*` (GitHub official) | First-party | **No** — version tags are fine |
| `github/*` (GitHub org) | First-party | **No** — version tags are fine |
| Same-repo / vendored (`./.github/actions/...`) | Not third-party supply chain | **No** as supply-chain pinning (still review in pwn-request context) |
| Org-owned / internal first-party actions when reviewing that org's repos | First-party for that org | **No** unless the action is external to the org's trust boundary |
| External orgs (aws-actions, docker, tj-actions, community, unknown) | Third-party | **Yes** — pin to full SHA when privilege makes it relevant |

When org ownership is unclear, treat non-`actions/*` / non-`github/*` / non-local refs as third-party.

---

## Pinning: Tags vs. SHAs

### Vulnerable: Tag References
### Reportable: Unpinned Third-Party Tags

```yaml
# VULNERABLE: Tag can be moved to point to malicious commit
- uses: actions/checkout@v4 # Tag — mutable
- uses: actions/checkout@main # Branch — mutable
- uses: actions/checkout@latest # Tag — mutable
- uses: some-org/some-action@v1 # Tag — mutable
# REPORT when third-party and the job is privileged enough (see severity)
- uses: some-org/some-action@v1 # Tag — mutable third-party
- uses: tj-actions/changed-files@v44
- uses: docker/build-push-action@v6
- uses: some-org/some-action@main # Branch — mutable
```

Tags are **mutable Git references**. The maintainer (or attacker with write access) can delete and recreate a tag pointing to a different commit. When the tag is updated, every workflow using that tag runs the new code.

### Safe: SHA Pinning
### Safe: Third-Party SHA Pinning

```yaml
# SAFE: Commit SHA is immutable
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.7
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
# SAFE for third-party: commit SHA is immutable
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
```

SHAs are **immutable** — once a commit exists, its SHA cannot change. Pin to the full 40-character SHA and add a comment with the version for readability.
SHAs are **immutable** — once a commit exists, its SHA cannot change. Pin third-party actions to the full 40-character SHA and add a comment with the version for readability.

### Do Not Report: First-Party Version Tags

```yaml
# NOT a supply-chain pinning finding
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/upload-artifact@v4
- uses: github/codeql-action/analyze@v3
- uses: ./.github/actions/local-build
```

First-party `actions/*` and `github/*` on version tags are not findings by themselves. Same-repo or vendored actions are not third-party supply-chain findings (they can still be unsafe if loaded from a PR-controlled checkout — that is a pwn-request / trust-crossing issue, not pinning).

---

## Attack Vectors

### Tag Mutation Attack

1. Attacker compromises a popular action's repository (phishing, leaked credentials, insider)
1. Attacker compromises a popular **third-party** action's repository (phishing, leaked credentials, insider)
2. Deletes the `v1` tag
3. Creates a new `v1` tag pointing to a malicious commit
4. Every workflow using `@v1` now runs the attacker's code

This is not theoretical — it's the primary supply chain risk for GitHub Actions.
This is not theoretical — CVE-2025-30066 (`tj-actions/changed-files`) and related incidents showed tag rewrites can leak secrets at scale.

### Account Takeover / Org Compromise

If an action author's GitHub account is compromised:
If a third-party action author's GitHub account is compromised:
- All actions under that account can be backdoored
- Version tags can be silently updated
- Users won't notice unless they're pinned to SHAs
Expand All @@ -61,73 +90,98 @@ If an action author's GitHub account is compromised:
Some actions download and execute external scripts at runtime:

```yaml
# Action's action.yml — RISKY
# Action's action.yml — RISKY even when SHA-pinned
runs:
using: composite
steps:
- run: curl -sSfL https://example.com/install.sh | bash
shell: bash
```

Even if you pin the action to a SHA, the external URL can change. The action itself is immutable, but its runtime dependencies are not.
Even if you pin the action to a SHA, the external URL can change. The action itself is immutable, but its runtime dependencies are not. Report this for **third-party** actions in privileged jobs; do not use it as a reason to demand SHA pins on first-party GitHub actions.

---

## Detection Patterns

```bash
# Find all action references
grep -rn "uses:" .github/workflows/ | grep -v "#"
# Find third-party action references (exclude first-party + local)
grep -rn "uses:" .github/workflows/ \
| grep -v "#" \
| grep -v "uses: \\.\\/" \
| grep -v "actions/" \
| grep -v "github/"

# Among third-party refs, find unpinned tags/branches (not full SHA)
grep -rn "uses:" .github/workflows/ \
| grep -v "#" \
| grep -v "uses: \\.\\/" \
| grep -v "actions/" \
| grep -v "github/" \
| grep -v "@[0-9a-f]\\{40\\}"

# Find third-party actions pinned to branch names
grep -rn "uses:" .github/workflows/ \
| grep -v "actions/\\|github/\\|\\./" \
| grep -E "@(main|master|develop|latest)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detection greps miss third-party actions

Medium Severity

The new third-party filters use substring matches on actions/ and github/, so they drop common external actions whose names contain those strings — including tj-actions/*, aws-actions/*, and google-github-actions/*. Reviewers following these greps can miss unpinned supply-chain findings the rest of the doc still treats as reportable.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7e127fe. Configure here.

```

Do **not** treat every non-SHA `uses:` as a finding. Filter to third-party first, then assess job privilege.

# Find unpinned actions (tags, branches)
grep -rn "uses:" .github/workflows/ | grep -v "@[0-9a-f]\{40\}" | grep -v "uses: \.\/"
---

# Find actions pinned to branch names
grep -rn "uses:" .github/workflows/ | grep -E "@(main|master|develop|latest)"
## When To Report

# Find actions from less-known orgs (not actions/ or github/)
grep -rn "uses:" .github/workflows/ | grep -v "actions/\|github/\|\./"
Report mutable third-party actions only when job privilege makes compromise security-relevant.

# Check if any actions curl at runtime
# (Requires reading the action's source — note this for manual review)
```
| Shape | Severity |
|-------|----------|
| Mutable third-party ref in package publishing, release signing, protected-branch push, production deploy, or token-minting job | **High** / **Critical** (unknown org + `pull_request_target` / secrets → Critical) |
| Mutable third-party ref with secrets, OIDC, or non-trivial write-scoped `GITHUB_TOKEN` | **High** or **Medium** depending on blast radius |
| Pinned third-party action that downloads and executes mutable remote scripts in a privileged job | **Medium**, or **High** when the downloaded payload runs inside the privileged step |
| Mutable third-party ref in public read-only CI with no secrets and no write scopes | **No finding** unless adjacent to another traced workflow risk |
| Unpinned first-party `actions/*` / `github/*` version tags | **No finding** |
| Local / vendored action pinning | **No finding** as supply chain (review pwn-request separately) |

---

## Risk Assessment by Action Source
## Risk Assessment by Third-Party Source

Use this only after the ref is classified third-party and the job is privileged enough to report:

| Source | Risk | Action |
|--------|------|--------|
| `actions/*` (GitHub official) | Low | Pin to SHA (defense in depth) |
| `github/*` (GitHub org) | Low | Pin to SHA |
| Major orgs (aws-actions, google-github-actions, docker) | Medium | Pin to SHA |
| Popular community actions (1k+ stars) | Medium | Pin to SHA, review source |
| Less-known actions (under 100 stars) | High | Pin to SHA, review source carefully, consider vendoring |
| Unknown / single-maintainer | Critical | Vendor locally or replace with inline `run:` |

---

## The Fix: SHA Pinning with Version Comments
## The Fix: SHA Pin Third-Party Actions

```yaml
steps:
# Pin to SHA, comment with version for readability
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.7
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
# First-party: version tags are fine — do not flag
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

# Third-party: pin to SHA, comment with version for readability
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
```

### Automated Pinning

Tools that automatically pin and update action SHAs:
Tools that can pin and update **third-party** action SHAs:

- **Dependabot** — GitHub native, updates action SHAs
- **Renovate** — Can pin and update actions
- **StepSecurity Secure Workflows** — Pins all actions to SHAs
- **StepSecurity Secure Workflows** — Can pin actions to SHAs (configure to match third-party-only policy if you do not want first-party pins)

### Vendoring Critical Actions

For high-security workflows, vendor the action locally:
For high-security workflows, vendor the third-party action locally:

```yaml
# Instead of: uses: some-org/critical-action@v1
Expand All @@ -141,21 +195,22 @@ For high-security workflows, vendor the action locally:

| Pattern | Severity | Rationale |
|---------|----------|-----------|
| Unpinned action from unknown org used in `pull_request_target` | **Critical** | Attacker could backdoor the action AND access secrets |
| Unpinned action from known org in sensitive workflow | **High** | Tag mutation risk with secret exposure |
| Unpinned action from GitHub official (`actions/*`) | **Medium** | Low risk of compromise, but defense in depth |
| Action that curls external scripts at runtime | **High** | Even SHA-pinned actions can be compromised via external deps |
| Local action (`./.github/actions/`) | **Low** | Controlled by repo, only risky in pwn request context |
| Unpinned third-party action from unknown org used in `pull_request_target` | **Critical** | Attacker could backdoor the action AND access secrets |
| Unpinned third-party action from known org in sensitive/privileged workflow | **High** | Tag mutation risk with secret exposure |
| Unpinned third-party action with limited privilege but real write/secret surface | **Medium** | Real supply-chain risk, bounded blast radius |
| Third-party action that curls external scripts at runtime in a privileged job | **High** | Even SHA-pinned actions can be compromised via external deps |
| Unpinned first-party `actions/*` / `github/*` | **Do not report** | Outside pinning policy |
| Local action (`./.github/actions/`) unpinned | **Do not report** as pinning | Controlled by repo; only risky in pwn-request context |

---

## Exploitation Scenario Template

```
ATTACK: Supply Chain via [unpinned action / tag mutation / curl|bash]
ENTRY: Attacker compromises [action repo / account / external URL]
ATTACK: Supply Chain via [unpinned third-party action / tag mutation / curl|bash]
ENTRY: Attacker compromises [third-party action repo / account / external URL]
PAYLOAD: Malicious code in [action.yml / downloaded script]
TRIGGER: Workflow [file:line] uses [action@tag] without SHA pin
TRIGGER: Workflow [file:line] uses third-party [action@tag] without SHA pin
EXECUTION: Modified action runs with workflow permissions
IMPACT: [RCE with workflow permissions, secret theft, etc.]
```
Expand Down
Loading