GitHub Actions is a modern approach to deploying our code to production, it wraps up the compute and security aspects of running deployment servers and exposes them to us through an API, allowing us to define our pipelines as code with YAML workflows.

There are a bunch of security benefits to be had through leveraging a platform which has a managed service for deployments, with the added benefit of allowing us to reduce our maintenance burden. However, one facet of GitHub Actions that can potentially expose you to a supply chain attack is the use of third-party Actions.

Third-Party GitHub Actions can be used to bundle up repetitive functionality we would otherwise need to add to our GitHub Workflow files each time, which would make them difficult to maintain. GitHub provide their own Actions for popular use cases such as checking out the repository code.

jobs:
 compile:
   runs-on: ubuntu-latest
   steps:
     - name: 'Checking out from GitHub'
       uses: 'actions/checkout@v4.2.2'

You can think of a third-party GitHub Action as you would a third-party package in most modern programming languages; they are brilliant building blocks, but they also introduce a new element of risk.

It’s always best to stick to trusted sources when importing third-party GitHub Actions, but even the most trusted sources are not immune to compromise. To mitigate the risks associated with importing third-party GitHub Actions we can pin the Action’s version tags to the commit SHA hash.

You can navigate to the repository for the third-party GitHub Action you want to use, and find the available tagged releases. Here is the tag for our example:

Shows an example version tag within the GitHub user interface

Verifying the Action you are importing is from the expected source repository and not a fork is also covered when we find the appropriate release tag.

As this tag isn’t immutable we want to swap out the release tag we used earlier with the commit hash, which you can see in the abbreviated version in the above image. Click the link to open up the commit associated with that release, from there we can grab the full hash we need to pin our version securely.

Shows where you can copy the full hash for a version commit within the GitHub user interface

Once we copy the full hash we can now replace the version in our workflow file with the hash for that version:

jobs:
 compile:
   runs-on: ubuntu-latest
   steps:
     - name: 'Checking out from GitHub'
       uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683'

Now if the third-party GitHub Action were to be compromised we won’t be impacted as the hash of future commits wouldn’t be the same, an attacker would have to create a SHA-1 collision for a valid commit, which is highly improbable.

If your repository is public in GitHub or you have access to GitHub Advanced Security features there is now a Code Scanning option to scan your Workflow files with CodeQL. This will produce findings when third-party GitHub Actions have been used without pinning to a specific commit hash.

Unfortunately automating updates of third-party GitHub Actions is not supported by dependabot at the time of writing, but if you want to avoid manually updating hashes for your Actions you can leverage Renovate as a like-for-like replacement for dependabot.

Useful Resources