GitHub Actions are great! But following some recent news, I wanted to make sure I understood their potential issues… and more importantly, get a better understanding of what I need to be looking for and avoiding. I found both the reporting around the exploit and the GitHub docs a little challenging to understand concretely where the issues lie.
It’s all about avoiding workflows that both have something valuable and can execute attacker-controlled code. The valuable thing in this context is almost certainly a credential with access: tokens, API keys, secrets, etc. And the whole point of pull request CI is… to execute code that someone unknown submits! There are a few key principles that help keep these separated:
-
The standard
on:pull_request:runs triggered from forks don’t get any repository secrets at all (beyond a read-only GitHub token). This is great! But it means, though, that these actions can’t do anything that change the state of the repository beyond their resulting status. They cannot make comments. They can’t add labels. They can’t assign reviewers or upload website previews. -
So that’s the whole point of the
on:pull_request_target:trigger; it allows actions with the target repository’s secrets available! This enables comments and labeling and such, but this comes with a HUGE caveat: it might also execute code that an attacker directly provides on their branch. Even worse,pull_request_targetautomatically escalates the default GitHub token’s permissions tocontents: writeunless you have an explicitpermissionssection. There’s one bit of protection here: GitHub ignores the changes the pull request makes to the particularworkflow.ymlfile itself. It always uses the file as it is on the default repository branch instead. This protection is only for that one file, so the fundamental rule forpull_request_targets are that they should not reference or include or execute any other files if their secrets use any sort of privileges.This is how trivy was initially compromised
Trivy had an
on: pull_request_targetworkflow to help comment, label, and assign issues. They’re a security conscious org — they knew this was tricky and explicitly commented in the yml to be careful! But earlier that month, a refactor had split out a go setup step into a separate action that was within the repository,uses: ./.github/actions/setup-go.yml. So the attacker changed that file, which was not subject to GitHub’s protections, and opened a PR which promptly dumped the runner’s memory and likely grabbed the org-scoped token they were using to assign reviewers. ref
-
But then of course actions often run other code, too, beyond the things that you yourself wrote and trust within your own repository. These are often actions themselves and a step simply
usesthem. These actions can run in even more trusted contexts, includingpush:. So again, like any dependency, you want to be sure you know what you’re using, that you trust it and its author/org, and you can limit your possible attack surface. It may be good practice to pin to an exact commit instead of a major@v5tag (or even@v5.1.2).This is how the trivy attack spread through other repos that
use:its actionOne particularly pernicious point in this attack was that GitHub tags/releases are (by default) not immutable. So the Trivy attacker was able to simply relabel existing releases with
contents: writeaccess in a much less noisy manner than creating a new release would be. Further, they pointed them to a commit on a fork — again, much more subtle than pushing to the base repository. All this subtlety allowed for more time to pass while downstream repositories fired off their privileged and trusted actions. The only thing that could’ve prevented downstream uses from compromise was an explicitly tagged commit. -
I think it’s also worth explicitly noting that workflow
steps aren’t securely isolated from each other, even though you can independently pass secrets to one step but not the other. Any secret that’s used within the workflow at large could be exfiltrated through a memory dump. Independent workflows, however, are isolated and each only gets the secrets it explicitly references in its yml file (and only that file).
The absolute scariest case here is the pull_request_target workflow that has access to an important secret (the default case!) and includes some execution from elsewhere outside that file. I also think it’s worth limiting all three things as much as possible, because code changes and it’s easy to, e.g., mistakenly add some misdirection or a secret in a gradual manner or otherwise break that fragile isolation… and there’s always other dependency attacks waiting in the future.
So take some time and review your token permissions and workflows today! ![]()