Connor's Blog

GitHub "Actions" Are An Impending Security Disaster

You may already be familiar with GitHub’s CI/CD offering, GitHub Actions. Confusingly, “Actions” refers not only to the CI/CD platform, but also reusable steps within workflows that anyone can publish on the Marketplace.

Actions are published through ordinary repositories hosted on GitHub.

GitHub itself provides many useful official Actions, such as checkout which checks out a repository. It can be used like this:

- name: Checkout
  uses: actions/checkout@v4

actions/checkout refers to the organisation and repository where the Action code is hosted, and v4 refers to the git tag to use. Herein lies, in my opinion, a severe issue with Actions.

Since git tags can be moved arbitrarily by the author, there’s nothing stopping them going rogue and changing the Action to be malicious at any time. The author could change the code to exfiltrate the repository itself, the GITHUB_TOKEN (which in many cases can have more power than it should), or other CI/CD secrets.

I’m not the first one to bring this up. Some developers are aware of this, but many aren’t. If you check the pipelines in popular repositories, some pin the Actions to specific commit hashes.

Zed’s CI pipeline can be taken as an example:

- name: Checkout repo
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
  with:
    clean: false
    fetch-depth: 0 # fetch full history

This does solve for the case of an Action author going rogue and immediately compromising every downstream pipeline using it, but it’s an extra step developers have to take to be secure, and they may not necessarily know they need to do this.

GitHub should provide a global tag ’locking’ mechanism, as many package managers already do. Go’s built-in package manager by default uses the checksum database, which prevents a tag from being moved once a package has been pulled publicly.

Since the Jia Tan backdoor, supply chain security has come in to a sharper focus. Yet, low hanging fruit like this hasn’t really received any attention. I have no doubt there’s an army of Jia Tans out there working on useful Actions in the hope to leverage them in an attack later on. They could also compromise a developer’s account and leverage theirs.

GitHub has a setting to restrict the Actions used within an organisation. The most permissive policy allows all Actions, and the least permissive allows only whitelisted Actions.

This feature is useful but it’s still not good enough, because the secure option is not the default. This is what I call opt-in security. It’s a mistake the software industry keeps making time again, and I would put it up there alongside the Billion Dollar Mistake.

GitHub should make urgent changes to mitigate Action supply chain attacks or I think we’re going to see a mass compromise sooner or later. I’m actually surprised there hasn’t been one already.

A blog post I came across a few days ago discussing Actions security in another context, mentioned Zizmor as a tool for catching common Actions workflow mistakes. (HN discussion)

Zizmor’s checks includes unpinned Action usages when ran in pedantic mode (-p):

help[unpinned-uses]: unpinned action reference
  --> /Users/dev/Documents/Projects/iapc/.github/workflows/build.yml:16:9
   |
16 |         uses: actions/setup-go@v5
   |         ------------------------- help: action is not pinned to a hash ref
   |
   = note: audit confidence → High

Update

After posting this I discovered another abusable behaviour of Actions. Due to the way GitHub works, and has worked for a long time, any commits to a fork of a repository are “attached” to the original upstream repository.

This behaviour has been abused before for trolling, like the “Linux backdoor.” The commit that introduced this code was not to the torvalds/linux repo, but a fork.

From my testing I discovered that in Actions workflows, it does not verify that an Action’s pinned commit hash actually belongs to the referenced repository.

I forked actions/checkout to print 1337 1337 1337 1337 1337 1337 1337 when it runs. Then I created a workflow that referenced the original Action and my forked action, but pinned both them to the HEAD commit hash of my modified fork.

- uses: actions/checkout@b3e85105c7b231b263ea050d77d78ebcc0f4c4b1
- uses: cedws/checkout@b3e85105c7b231b263ea050d77d78ebcc0f4c4b1

And here’s the output:

As you can see, the modified string printed in both steps, which indicates it ran my modified code in both cases even though the first step appears to reference GitHub’s official action.

Why is this dangerous? Because it makes it much easier for sneak malicious code in via pull requests. It could also be leveraged by a disgruntled employee to create a backdoor that would be difficult to notice.

If one checks the Action by navigating to this commit hash by URL, they may spot a warning at the top of the page warning that the commit belongs to a fork, but it could also go unnoticed.

I have reported this to GitHub, but last I heard they don’t consider this kind of thing a vulnerability (lolwat?), so I’ll see what they say.

Edit: as predicted GitHub said it’s expected behaviour…