Weaponizing Dependabot: Pwn Request at its finest

Sébastien Graveline
Comic book style illustration of a confused Wild West deputy, GitHub's Dependabot, about to press an 'Auto-Merge' lever on a machine releasing gremlin-like 'Vulnerabilities'.

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.

Introduction

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.

Meet Dependabot: Your Automated Dependency Butler

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.

The "Confused Deputy" Problem: Wait, Who Asked You To Do That?

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:

  1. Fork It: Attacker forks a target repository. This repo has one of those "auto-merge if Dependabot" workflows.
  2. Rig it: Attacker adds their malicious payload to the Default branch of their fork.
  3. Wake Up, Dependabot!: Attacker enables Dependabot on their fork and adds an obviously outdated dependency to get its attention (often at the time you fork, this pre-requisite will already be met).
  4. Dependabot Does Its Thing: Dependabot creates its update branch on the fork. Since this branch is based on the fork's own Default branch, the now includes the attacker's payload (through the refs/pulls/ read-only namespace).
  5. The Bait: Attacker creates a Pull Request from this weaponized Dependabot branch on their fork, targeting the original (victim) repository.
  6. Firing blanks: This new PR triggers the auto-merge workflow in the victim repo. However, at this point, github.actor is the attacker, so the if condition fails, and the workflow doesn't merge anything.
  7. Confused, are you?: Attacker goes back to the original Pull Request in their fork that Dependabot initially opened and comments: @dependabot recreate.
  8. Dependabot Obeys: Dependabot goes to work and recreates its branch and force-pushes the changes. This triggers the vulnerable pull_request_target workflow again, but this time with the synchronize event.
  9. Bingo!: Now, the if: ${ { github.actor == 'dependabot[bot]' }} condition is true! The workflow promptly merges the attacker's code.

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!

Level Up: Dependabot Deputy Confusion Injection

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:

  1. The Merge Conflict Tango
  2. The @dependabot merge Shuffle with a Custom Default Branch (Kudos to some clever participants at the Montréhack hackathon of May 2025 for coming up with this one as they were playing the second round of our MessyPoutine CTF!)

Technique 1: The Merge Conflict Tango

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:

  1. The Setup: Fork the target repo (with the vulnerable workflow), enable Dependabot, and get it to create its update PR/branch.
  2. Create Conflict: On your fork, introduce a file with the same name but different content to both Dependabot's branch and your fork's default branch. This manufactures a merge conflict.
  3. Preserve Dependabot's Work (Sort Of): Figure out which file Dependabot originally changed (We can see them in its PR). Revert that specific change on Dependabot's branch by uploading the original version from the default branch. This is key to stop Dependabot from thinking its job is done and closing its PR later.
  4. Switcheroo: Change the default branch of your forked repository to Dependabot's branch.
  5. Resolve This!: Go to the newly conflicted Pull Request (the one Dependabot made). GitHub's UI will offer to help you resolve the conflict.
  6. The Magic Button: Fix the conflict. When you click "Commit merge," you'll get a very special dialog: "Commit updates to the ... branch" OR "Create a new branch and commit updates. Your pull request will be updated automatically.". To be honest, we had never seen this dialog on github.com before we had to develop this TTP (Thanks @AdnanKhan for the hint). 
  7. Payload Branch: Choose option #2! Name this new branch with your desired payload (e.g., foo-$(id)-bar).
  8. New PR, Same Story (Almost): Create a new Pull Request from this payload-named branch, targeting the original victim repository. This triggers the workflow, but github.actor is still you, the attacker. No execution yet.
  9. The @dependabot recreate: Comment @dependabot recreate on the original Dependabot Pull Request.
  10. Execution!: Dependabot will now recreate and force-push its changes to your payload-named branch (because its PR is now pointing there). This triggers pull_request_target with synchronize, github.actor is dependabot[bot], and your injected branch name gets executed!
This technique can be a bit finicky – one wrong step, and you might have to start over, but don't worry we were able to fully script it. Also, some characters are off-limits for branch names:

Technique 2: The Default Branch Merge Shuffle

This one's a bit more straightforward:

  1. Standard Setup: Fork, enable Dependabot, let it create its branch / PR.
  2. Crafty Branch: On your fork, create a new branch named with your payload (e.g., branch-$(id)-inject).
  3. Default Branch Swap: Change the default branch of your forked repository to this new payload-named branch.
  4. Initiate PR (No Merge Yet): Create a Pull Request from this new payload-named default branch, targeting the victim repository. Again, github.actor is you, so the job is skipped.
  5. The @dependabot merge Command: Go to the original Dependabot PR and comment: @dependabot merge.
  6. Command Execution: Dependabot will merge its own changes into what it thinks is the standard default branch. But because you swapped it, it's merging into your payload-named branch! This merge event changes the reference, triggers pull_request_target with synchronize, github.actor is dependabot[bot], and your malicious branch name (which is now github.event.pull_request.head.ref) gets executed by the vulnerable workflow step.

Bonus Round: Bypassing Branch Protection!


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:

  1. The Setup: The attacker somehow got contents: write on the target repository + Dependabot is on the Branch Protection (or Repository Ruleset) bypass list for the Default branch + there's an legitimate Dependabot PR already opened.
  2. Malicious Push: Attacker pushes their desired payload directly to Dependabot's branch in the target repository.
  3. The Command: Attacker comments @dependabot merge on Dependabot's PR (using the identity with contents: write).
  4. Bypass Achieved: Dependabot, using its bypass privileges, merges its (now malicious) branch into the protected default branch.

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?

  1. Compromised PAT: If an attacker got their hands on a Personal Access Token (PAT) with the necessary permissions, they can make the comment directly.
  2. TOCTOU with GITHUB_TOKEN: If a GITHUB_TOKEN is compromised, the attacker might need to win a Time-of-Check-vs-Time-of-Use (TOCTOU) race. They would wait for a legitimate user to innocently comment @dependabot merge. Then they can push their malicious code to Dependabot's branch just in time for the merge. Dependabot can sometimes be quick, but merges taking 20+ seconds have been observed – plenty of time for a swift push - we've successfully used the ActionsTOCTOU tool for this kind of scenario.

Why Pick on Dependabot?

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:

  • ✅ It's Trusted
  • ✅ It's Controllable (by Anyone, Indirectly)
  • ✅ People Really Want to Automate It

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.

How can I protect myself?

While these scenarios might not be as common as other Pwn Request flavors, they've definitely shown up in critical projects and big organizations. 

Detection

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.

Prevention

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:

Confusable or Forgeable Contexts (Approach with Extreme Caution!)

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

Most likely NOT confusable

Event

Activity type(s)

Safer Elements to Check User Identity

pull_request_target

NOT synchronize 

github.event.pull_request.user.login,
github.event.pull_request.user.id

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.

To prevent branch protection bypasses

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.

Auto-merge

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.

Want to Get Your Hands Dirty?

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!

SLSA dip — It’s Build Time!

Image of BoostSecurity.io
BoostSecurity.io

This article is part of a series about the security of the software supply chain. Each article will...

Read more
Wolfi, whose name was inspired by the world’s smallest octopus

The tale of a Supply Chain near-miss incident

Image of François Proulx
François Proulx

TL;DR: We disclosed to Chainguard in December 2023 that one of their GitHub Actions workflow was...

Read more