Dev.to Security 🔐 Cybersecurity 👁 0 📖 2 min read

pin-deep: you pinned your Actions, but your Actions didn't pin theirs

Everyone knows the drill: pin your GitHub Actions uses: to a commit SHA so a compromised tag can't rug-pull your CI. Done, right? Not quite. There's a depth problem nobody checks. The tj-actions incident (CVE-2025-30066

Everyone knows the drill: pin your GitHub Actions uses: to a commit SHA so a compromised tag can't rug-pull your CI. Done, right?

Not quite. There's a depth problem nobody checks. The tj-actions incident (CVE-2025-30066) is the textbook case: the compromise reached victims through reviewdog, which referenced the compromised action. Pinning reviewdog itself wasn't enough — the hole was one level deeper, inside the dependency of a pinned dependency.

"You pinned your Actions, but the Actions you pinned didn't pin their dependencies."

That's what pin-deep checks. Give it a workflow file or a single pinned uses: value, and it recursively fetches the action.yml of every SHA-pinned action — up to a configurable depth, with cycle protection — and flags every nested uses: that isn't pinned to a full commit SHA.

$ pip install pin-deep
$ pin-deep .github/workflows/ci.yml
[unpinned] depth=2 actions/cache@v3
    source: action.yml of actions/setup-python@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    chain:  acme/root@aaa...  ->  actions/setup-python@aaa...  ->  actions/cache@v3
    note:   not pinned to a full commit SHA

A few design decisions worth knowing:

  • Only full SHAs get recursed into. A floating tag has no fixed content to analyze — auditing its children would audit today's state, not the state you ship. Unpinned refs are flagged, never followed.
  • Short SHAs are warnings, not passes. Seven hex chars technically resolve, but they're weaker than a full 40/64-char SHA. --strict fails CI on those too.
  • Depth defaults to 3. Deep enough to catch the reviewdog-shaped hole, shallow enough to finish before your coffee does.
  • Exit code 1 on findings, so it drops straight into CI. GITHUB_TOKEN is used for the API fallback; public repos need nothing.

Differentiation

Static workflow linters like zizmor analyze your uses: lines. pin-deep checks one level further: what your pinned actions themselves depend on, recursively. Nobody else does the recursive nested check — pinning has a depth dimension, and this is the tool for it.

stdlib only, no dependencies. If it finds a hole in your supply chain, you'll know exactly which chain of actions put it there.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.