Overview
I identified a High risk vulnerability impacting GitHub’s dependabot-core
repository that could have allowed an attacker to conduct a supply chain attack on GitHub users by backdooring the Dependabot containers. The cause of the vulnerability was a race condition in a workflow that maintainers would trigger to perform integration testing on approved pull requests prior to merging.
Background
Continuous Integration / Continuous Delivery (CI/CD) pipelines for Open-Source (and even closed source!) projects have a concept of an approval gate. This is a step that an authorized actor must perform before pipeline execution can continue. This pattern is common for running integration tests that require access to secrets or creating preview environments on code originating from fork pull requests.
The attack would work like this:
- Contributor creates a pull request
- Maintainer reviews the code
- Maintainer triggers privileged CI operation
In GitHub Actions, we will typically see workflows that use approval gates in conjunction with pull_request_target
or issue_comment
events.
There is another trigger that maintainers will use and that is the workflow_dispatch
trigger. This trigger is often susceptible to a Time-of-Check-Time-of-Use vulnerability that is similar to the issue_comment
trigger.
For bug bounty hunters, a valid workflow_dispatch
TOCTOU will typically be a Medium or High risk vulnerability. This is because the attack complexity will always be high and it requires interaction. If you are reporting a workflow_dispatch TOCTOU, make sure there is impact - don’t bother reporting a GitHub Actions TOCTOU if the impact is limited to pull-requests: write
only and there are no Cache Poisoning escalation paths.
Actions
I want to provide a bit of background on actions’ security boundaries for those who might not be familiar.
In GitHub Actions, there are two types of workflows: privileged and unprivileged fork workflows.
Workflows that run on the following triggers from forks are always unprivileged:
pull_request
pull_request_review_comment
pull_request_review
This means that they cannot have access to secrets and they cannot have a github token with write access.
On the other hand, workflows that run on every other trigger are privileged. They run in the context of the base repository, can have access to secrets, and can have access to a privileged github token.
The workflow_dispatch
trigger is privileged. One common pattern I’ve seen is that maintainers will create a workflow that runs on workflow_dispatch
and takes a pull request number. This is a common pattern for triggering infrequent, long running test jobs on pull requests that require access to credentials.
Let’s Take It Deeper
Dependabot/dependabot-core
While working on the Gato-X version 1.0 rewrite, which included an overhauled static analysis algorithm, I added a detection for workflow_dispatch
TOCTOU vulnerabilities. I scanned a few large organizations and I got a hit for the dependabot/dependabot-core
repository.
The issue existed within the .github/workflows/images-branch.yml
workflow. It ran on the workflow_dispatch
trigger and checked out code from the pull request head:
In the Build ecosystem image
step, we can see that the workflow runs a bash script located at script/build. Due to the previous checkout, this code contained the latest state of the pull request at the moment of executing gh pr checkout ${{ inputs.pr }}
within the Prepare tag (forks)
step.
Exploit Preconditions
When analyzing a bug lead for a workflow_dispatch
TOCTOU, it is important to check if a maintainer would run the workflow as part of a normal interaction with an external contributor.
In some cases you will encounter workflows that take a pull request number as a parameter but the workflow is only used to merge a specific backport pull request created by a bot account. In this case, it is not a vulnerability because the maintainer would never run the workflow on an external contributor fork pull request.
In this case, I found examples of previous pull requests where the creator was an external contributor and the maintainers ran this workflow. Bingo! This established feasibility for this vulnerability.
Looking at the diffs in https://github.com/dependabot/dependabot-core/pull/10829/files, the level of effort is not excessive to make a meaningful change. Given what an attacker could get from exploiting vulnerability it is well within the realm of what a threat actor would be willing to do.
Winning the Race Condition
The next step here would be to win a race condition. The race window here existed between the check at the end of the approval job (the repository had the protection setting of dismissing approvals if the PR was updated) and the gh pr checkout
within the push-updater-images job.
|
|
To estimate the race window, I looked at the workflow timestamps from PR 10829. The job ended at 10:07:29.1026629Z
|
|
We can see in the logs for https://github.com/dependabot/dependabot-core/actions/runs/12429468333/job/34703034734, that the workflow does not check out the PR until 10:07:48.5908963Z. That leaves a race window of almost 19 seconds, which is wide enough to comfortably win via automated or manual methods.
|
|
An attacker can write a script to poll for workflow_dispatch
events on that workflow as soon as they see the maintainer approve their pull request (or start refreshing at that point with their Git push ready to fire, either works). In this example, the maintainer approved the workflow at 9:59 UTC, and then ran the workflow roughly 8 minutes later.
Achieving Impact
If the attacker can successfully won the race condition, then the code they modify within script/build will run within the workflow. They could simply modify the build script to download and replace source code files (or do a git checkout of a different SHA within the fork network - many ways to change code files after achieving execution).
Even looking at the build produced from that specific workflow (https://github.com/dependabot/dependabot-core/actions/runs/12429468333) we see that there were 35k downloads, so even the trivial poisoning approach within a build would net fairly widespread impact.
|
|
The vulnerable workflow used a GITHUB_TOKEN
with packages: write
and id-token: write
, so an attacker can publish signed containers to GHCR. Ultimately, the impact of this vulnerability is that an attacker can backdoor Dependabot updater images on GHCR, which could lead to broad arbitrary code execution within Dependabot updater jobs in both public and private repositories.
Dependabot updater images do not have access to other private repositories or even the GITHUB_TOKEN
, but they do have access to the repo clone it is running on at DEPENDABOT_REPO_CONTENTS_PATH
. Since this backdoor would be specific to the Dependabot images, even if an attacker cannot escape from the container, they could add code in it to compress the cloned repository and exfiltrate it to an attacker’s servers. This would lead to a loss of private source code for GitHub customers who use Dependabot.
GitHub’s Fix
GitHub fixed this issue in the following pull request: Ensure additional commits after approval are rejected
Now, the workflows saves off the commit SHA attached to the approval decision.
|
|
After checking out the pull request, the workflow verifies that the SHA matches the HEAD SHA of the pull request. If the SHA changes, it means the pull request creator added new code in an attempt to exploit the race condition.
|
|
Disclosure Timeline
Kudos to GitHub for triaging the report and addressing the issue so quickly!
- January 1st. Submitted Report
- January 3rd. Fixed
- January 31st. Awarded bounty.
Conclusion
It’s important to look at more than just the usual suspects of issue_comment
, pull_request_target
, and workflow_run
when looking for Poisoned Pipeline Execution vulnerabilities.
This is especially important as awareness of the risks surrounding pull_request_target
and issue_comment
grows. Maintainers might opt for workflow_dispatch
triggered workflows because they are safer, but forget to account for race conditions.
Wondering how you can find these vulnerabilities? Head over to the Gato-X repository and install the tool. Gato-X scans for workflow dispatch TOCTOU events along with several other GitHub Actions vulnerabilities.