Skip to content
· 5 min read HIGH @Sdmrf

I Audited Our GitHub Actions and Now I Can't Sleep

A look at the supply chain risks hiding in your CI/CD pipelines. Spoiler: it's worse than you think.

On this page

Last month I decided to actually look at what third-party GitHub Actions we use across our repos.

I wish I hadn’t.

The Audit

Started simple: just list all the third-party Actions used in our workflows.

# Find all GitHub Actions across repos
grep -r "uses:" .github/workflows/ | grep -v "actions/" | sort -u

Found 47 distinct third-party Actions across 23 repositories.

Then I asked some questions about each one:

  • Who maintains it?
  • When was it last updated?
  • Is it pinned to a SHA or a mutable tag?
  • What permissions does it request?
  • Do we actually need it?

The answers were… not great.

What I Found

1. Mutable Tags Everywhere

# What most of our workflows looked like
- uses: some-action/thing@v1

# What they should look like
- uses: some-action/thing@a1b2c3d4e5f6...

Out of 47 Actions, 41 were pinned to tags like v1, v2, latest.

This means: the maintainer can push a malicious update, and next time our workflow runs, we execute their code with no review.

2. Abandoned Projects

Several Actions we depended on:

  • Last updated 2+ years ago
  • Maintainer hadn’t responded to issues in 18 months
  • Had open security issues with no fixes

We were running this code in CI with access to our secrets.

3. Excessive Permissions

Found an Action we used for “formatting checks” that requested:

  • contents: write
  • pull-requests: write
  • issues: write

For formatting checks. Why?

Turns out it had some auto-fix feature nobody used that required write access. But every workflow run was granting these permissions whether the feature was used or not.

4. Dependency Chains

One Action I investigated:

Our workflow
  → uses action-a
    → internally uses action-b
      → internally uses action-c
        → runs npm install with package.json that pulls 200 packages

We were implicitly trusting hundreds of packages through a single uses: line.

5. Fork of Fork of Fork

Traced one Action’s history:

  1. Original project abandoned in 2021
  2. Fork created, maintained briefly, abandoned 2022
  3. Fork of fork created, that’s what we used
  4. Fork of fork’s maintainer: anonymous GitHub account with no other activity

We were running code from an anonymous stranger with access to our deployment secrets.

Why This Matters

GitHub Actions run in your CI/CD environment. Depending on your setup, they might have access to:

  • Source code
  • Deployment credentials
  • AWS/GCP/Azure keys
  • NPM/PyPI publish tokens
  • Production secrets
  • Customer data (if tests use real data)

A compromised Action can:

  • Steal all of the above
  • Inject code into your builds
  • Modify your releases
  • Persist access through compromised dependencies

This isn’t theoretical. We’ve seen it happen:

  • event-stream incident (2018)
  • ua-parser-js compromise (2021)
  • node-ipc malicious code (2022)
  • tj-actions/changed-files compromise (2023)

What We’re Doing About It

1. SHA Pinning

Every Action now pinned to full SHA:

# Before
- uses: actions/checkout@v4

# After
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Yes, it’s ugly. Yes, you have to update it manually. Yes, it’s worth it.

We use Dependabot to propose updates, then review before merging.

2. Vendoring Critical Actions

For critical workflows (releases, deployments), we forked and vendored the Actions:

# Instead of external
- uses: some-org/deploy-action@sha

# Use our fork
- uses: our-org/vendored-actions/deploy@sha

We control the code. We review changes. It’s extra work, but these workflows have production access.

3. Permission Minimization

Every workflow now has explicit, minimal permissions:

permissions:
  contents: read
  # Only add more if actually needed

Most Actions work fine with read-only access. The ones that don’t need scrutiny.

4. Action Allowlist

Implemented an allowlist of approved Actions. Workflows using non-approved Actions fail in CI.

This forces a review process for new Action additions.

5. Regular Audits

Quarterly review of:

  • What Actions are in use
  • Whether they’re still maintained
  • Whether we still need them

What You Should Do

If you haven’t audited your Actions recently:

Step 1: Inventory

find . -name "*.yml" -path "*/.github/workflows/*" -exec grep -l "uses:" {} \;

Step 2: Check pins

Are you using SHAs or tags? Tags are mutable.

Step 3: Review permissions

What can each workflow do? What can each Action access?

Step 4: Assess maintainers

Who are you trusting? Are they trustworthy?

Step 5: Minimize

Do you need that Action? Could you use a built-in alternative?

The Uncomfortable Truth

Modern software development requires trusting a lot of strangers.

Every npm install, every pip install, every uses: is an act of trust. We trust that:

  • The code does what it claims
  • The maintainer isn’t malicious
  • The maintainer’s account isn’t compromised
  • The dependencies are also trustworthy

Usually this trust is justified. But “usually” isn’t “always.”

The Codecov bash uploader incident, the SolarWinds compromise, the countless npm package hijacks - these aren’t anomalies. They’re the inevitable result of software supply chains built on implicit trust.

My Current Paranoia Level

After this audit:

  • Critical workflows (deployments, releases): Vendored Actions only, reviewed quarterly
  • Standard workflows (tests, linting): Approved Actions only, SHA-pinned
  • Development workflows (convenience stuff): Minimal permissions, monitored

Is this overkill for most organizations? Probably.

Is the alternative “hope nobody compromises the random Action you found on the marketplace”? Yes.

Pick your risk tolerance.


The uses: keyword is powerful. So is rm -rf /. Both deserve respect.

Related Articles