SLSA dip — It’s Build Time!
This article is part of a series about the security of the software supply chain. Each article will...
TL;DR: Your trusty Dependabot (and other GitHub bots) might be an unwitting accomplice. Through "Confused Deputy" attacks, they can be tricked into merging malicious code. This doesn’t stop here. It can escalate to full command injection via crafted branch names and even bypass branch protection rules. Plus, we disclose two new TTPs to build upon previously known techniques.
Ah, Dependabot! GitHub's built-in butler, tirelessly checks if your dependencies are fresh and, if not, prepares a PR with the updates. What a lifesaver, right? But, as with any cog in an automated system, there’s always the potential for rust. In this story, it is the deputy confusion attack.
So, how does this magic work? You can boss Dependabot around using a .github/dependabot.yml file (check out the Dependabot options reference).
Once active, Dependabot periodically scans your repo, guided by its config file. It also perks up and re-scans whenever you tweak this file or poke it via the https://github.com/<Owner>/<Repo>/network/updates tab.
When Dependabot spots an outdated dependency, it kicks off a workflow (you'll see "Dependabot Updates" pop up in your Actions tab). Then, it creates a new branch, usually named something like dependabot/<module_type>/<registry>/<package_name>/<new_version>. Finally, it opens a Pull Request to your default branch with all the suggested changes.
Now, let's talk about the Confused Deputy Problem. It's a classic vulnerability class (CWE-441, if you're into that sort of thing) where a trusted party (our "deputy") is tricked by an attacker into performing an action. The deputy thinks it’s doing honest work, but it's carrying out the attacker's nefarious plan.
Some workflows are sensitive – they might have special permissions or access to secrets, so many developers added a user check at the beginning of them. Just to make sure it was triggered by a trusted user.
Naturally, folks got a bit tired of manually approving every single Dependabot PR. "It's just a version update," they'd say. Others might say: "and we usually just end up blindly merging it anyway!". So, they created workflows to auto-merge PRs if the creator was Dependabot.
Something like this GitHub Actions workflow:on: pull_request_target
jobs:
auto-merge:
runs-on: ubuntu-latest if: ${ { github.actor == 'dependabot[bot]' }}
steps:
- run: gh pr merge $ -d -m
Seems safe, doesn't it? After all, Dependabot is trusted, and an attacker can't just force it to open a PR on a repo they don't control… Here's the trick: github.actor does not always refer to the actual creator of the Pull Request. It's the user who caused the latest event that triggered the workflow.
So, if an attacker could somehow force Dependabot into making a change that triggers this auto-merge workflow, they can merge their own malicious code.
Now, an attacker can't directly command Dependabot on a repo they don't have rights to. So, what can they make Dependabot do? Let's look at the events Dependabot is able to trigger:
Event |
Action |
From fork |
Command |
pull_request_target, pull_request |
When creating or updating a pull request. |
true |
@dependabot recreate |
issue_comment |
When showing information. |
false |
@dependabot show <dependency name> ignore conditions |
push |
When pushing to the branch, when merging etc. |
false |
@dependabot merge |
create |
When creating a new branch. |
false |
N/A |
delete |
When deleting a branch. |
false |
@dependabot close |
workflow_run |
It simply gets triggered as a side-effect of another (named) workflow running or completing. |
true |
Relevant if the parent workflow was triggered using one of the above commands. |
The juicy one here is that Dependabot can be forced to update a Pull Request, even one originating from a fork!
Here's the evil plan:
And just like that, our attacker has bypassed the user verification using Dependabot as their confused deputy! This technique originally discovered and documented brilliantly by Hugo Vincent in his seminal article GitHub Actions Exploitation: Dependabot, is a crafty way to hack Open Source projects, in a clever and unique way, different from your garden-variety Pwn Requests. It's not just theoretical either. This kind of exploit was exploited in the December 2024 Kong Ingress Controller attack and by using BoostSecurity's own Package Supply infrastructure, we keep finding instances at scale in mission-critical Open Source projects!
So we've shown how an attacker can trick Dependabot into merging a branch with bad stuff in it. This is essentially could be considered a variant of Pwn Request without RCE (if you're not familiar, check out our previous article called Exploiting CI/CD with Style(lint): LOTP Guide). But can we take it further? You bet! Other than untrusted code checkout execution, the other common Build Pipeline exploitation technique is an injection (as seen in Opening Pandora’s box - Supply Chain Insider Threats in Open Source projects). The most common flavour? Injecting by maliciously crafting a Git branch name.
Consider this simplified workflow snippet:on: pull_request_target
jobs:
just-printing-stuff:
runs-on: ubuntu-latest if: ${ { github.actor == 'dependabot[bot]' }}
steps:
- run: echo ${ { github.event.pull_request.head.ref }}
See that ${ { github.event.pull_request.head.ref }}? GitHub Actions will replace the content of the head.ref (the branch name the PR is coming from) directly in the run script. If an attacker calls their branch something like $(id), that command gets executed!
Normally, the head.ref is the attacker-controlled branch name from their fork. But in our Dependabot deputy confusion scenario, Dependabot has a very particular naming scheme for its branches (dependabot/<ecosystem>/...). Try to rename its branch directly, and Dependabot usually gives up and will cease to push to it anymore – which is a pre-requisite for that pull_request_target synchronize trigger.
So, you might think, "Phew, Dependabot deputy confusion is immune to injection Pwn Requests!" Wrong! So very wrong! BoostSecurity's research team (plus some clever folks during a Hackathon we organized) uncovered not one, but two unique sneaky ways to achieve this kind of injection - this is a previously undisclosed TTP we've had to develop in the fall of 2024 as it was key in several high-profile Bug Bounty responsible disclosures:
This method lets an attacker rename Dependabot's branch without breaking its connection to it. It's a bit of a dance and is definitely unstable, but it is repeatable.
Here are the steps:
This one's a bit more straightforward:
What else can our confused Dependabot be coerced into doing? How about sidestepping those Branch Protection rules?
Branch Protection is a nice security feature of GitHub. It lets you lay down the rules for important branches (like main or release branches), restricting who can push, requiring reviews, etc. For instance, you might only allow maintainers to push directly by preventing pushes and adding core maintainers to an "allow bypass" list.
Now, if projects have auto-merge workflows for Dependabot PRs, and branch protection is enabled on the target branch, guess who often needs to be on that bypass list? Yup, dependabot[bot].
See where this is going? If an attacker somehow gains contents: write permission (perhaps through an insider threat, Personal Access Token / SSH key theft or a second-order workflow attack on the victim repository itself), they can potentially bypass branch protection:
One wrinkle: Dependabot is a bit picky and refuses comments from GitHub Apps (see issue #9147). So, how can an attacker send that @dependabot merge command?
So, what makes Dependabot the prime candidate for these deputy confusion attacks?
Truth be told... not a whole lot that's unique. It boils down to:
So, could other bots be similarly confused? Absolutely. And they are. Dependabot is just super popular and deeply integrated into GitHub. But any public GitHub App that performs actions in a repository, and that users might want to automate via GitHub Actions, is potentially vulnerable to a deputy confusion attack.
Using our recently publicly discussed OSS Package Threat Hunting infrastructure, we discovered a list of bots that look like prime candidates for similar shenanigans. Stay tuned for more details in an upcoming article.
While these scenarios might not be as common as other Pwn Request flavors, they've definitely shown up in critical projects and big organizations.
Shameless plug time! You can use our BoostSecurity's own Build Pipeline static analysis scanner, poutine (please star it on GitHub), where we've recently Open Sourced a new rule to sniff out these kinds of vulnerabilities! Check out the new, aptly named Confused Deputy Auto-Merge rule.
To dodge deputy confusion, you need to be really careful about which GitHub context variables you trust, especially with events like pull_request_target.
Here’s a handy cheat sheet:
Event |
Activity type(s) |
Confusable/Forgeable Elements |
pull_request_target |
synchronize |
github.actor, github.triggering_actor, github.actor_id, github.event.pull_request.sender.login, github.event.pull_request.sender.id |
workflow_run |
Any without branches filter. |
github.actor, github.triggering_actor, github.actor_id, github.event.sender.login, github.event.sender.id, github.event.workflow_run.actor.id, github.event.workflow_run.actor.login, github.event.workflow_run.triggering_actor.id, github.event.workflow_run.triggering_actor.login |
workflow_run |
Any without branches filter. |
github.event.workflow_run.head_commit.author.name, github.event.workflow_run.head_commit.author.email |
Event |
Activity type(s) |
Safer Elements to Check User Identity |
pull_request_target |
NOT synchronize |
github.event.pull_request.user.login, |
Other events such as pull_request, issue_comment, push, create and delete can’t be triggered by Dependabot as an external user from a fork.
If Dependabot must have bypass privileges, ensure that Dependabot's own branches (e.g., dependabot/**) are also protected with similar restrictions as your main protected branch. This way, an attacker (could be Insider Threat) can't just push to Dependabot's branch and tag along for the ride.
Furthermore, some community Actions are designed to handle Dependabot merges more securely by specifically verifying Dependabot's identity or using its dedicated commands:
These can be safer alternatives to rolling out your own auto-merge logic.
Curious to see this vulnerability in action and understand its guts? We've set up a purposely vulnerable GitHub organization just for you: MessyPoutine.
Check out the gravy-overflow repository.
The workflow .github/workflows/level3.yml named Poutine Level 3, contains a prime example of deputy confusion waiting to be exploited. Go ahead, give it a try!
This article is part of a series about the security of the software supply chain. Each article will...
TL;DR: We disclosed to Chainguard in December 2023 that one of their GitHub Actions workflow was...