GitHub Actions Supply Chain Attack
In mid March, a massive supply chain attack compromised ~23,000 GitHub repositories via GitHub Actions.
Roughly four months prior, I predicted this attack would happen. But you didn’t have to be clairvoyant to see this coming.
I could write a post whining about GitHub’s security malaise and the design of Actions, but instead I’ll try to make this a productive one.
GitHub are working on a long-awaited feature called Immutable Actions that should make flash supply chain attacks like this impossible. It’s currently in preview and I’m not sure when it’s slated to be generally available.
For now, I would highly recommend forking any Actions you use into your own organisation or under your own account, ideally auditing the code you pull in before making use of them.
If that’s too much effort, you can run Frizbee against your repos to pin Action versions to a specific commit. It supports erroring (nonzero exit code) upon finding unpinned Actions too, so you could utilise this in a CI workflow to enforce pinning.
In an organisation, you can use the Require workflows to pass before merging repository rule to enforce that a workflow passes against all PRs in all repositories. It’s critical that repositories containing required workflows have adequate branch protection and required reviewers.
I created a required workflow at work to enforce pinning across the organisation. I didn’t use the frizbee-action because it rebuilds a Docker image on every run, which is too slow to run on every single PR. I cooked up something custom instead, feel free to steal it:
name: Frizbee
on:
pull_request:
jobs:
frizbee:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Install frizbee
env:
FRIZBEE_VERSION: 0.1.7
FRIZBEE_SHA256: 327b5ab60aea0df050c4c60602db1d27024b2e572c19815b6eeefc70402ae588
run: |
set -e
wget -O /tmp/frizbee.tar.gz https://github.com/stacklok/frizbee/releases/download/v${FRIZBEE_VERSION}/frizbee_${FRIZBEE_VERSION}_linux_amd64.tar.gz
echo "${FRIZBEE_SHA256} /tmp/frizbee.tar.gz" | sha256sum --check
tar -C /tmp -xf /tmp/frizbee.tar.gz
- name: Run frizbee
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
/tmp/frizbee actions --error
In my previous post I also mentioned Zizmor, a great tool for linting workflows for common security issues. In pedantic mode (-p), it will error upon finding unpinned Action usages.
Zizmor doesn’t have an Action so you’d also need to make something yourself to run this as a CI check. Unfortunately the author doesn’t publish binaries on GitHub - I’ve made a request for this but they’re happy with just publishing to PyPi for now.
I’m not comfortable with pulling Zizmor via this distribution method in a required workflow, but here’s something you can use if you don’t care about that:
name: Zizmor
on:
pull_request:
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
with:
cache: false
- name: Run zizmor
run: uvx zizmor@${ZIZMOR_VERSION} --format plain .
env:
ZIZMOR_VERSION: 1.6.0
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Long term, I’d like to look into whether it would be possible to do offline builds with custom runners. This of course wouldn’t prevent build artifact tampering, but it might at least save CI secrets from being leaked all over the internet.