Some vulnerabilities are interesting because of their complexity. Others are terrifying because of their simplicity. The GitHub Actions RCE reported against DuckDuckGo in 2026 falls firmly in the second category: one misconfigured workflow file, zero required privileges beyond a free GitHub account, and a straight path from a forked pull request to a poisoned release shipped inside every DuckDuckGo browser on the planet.
This is my original analysis of a disclosed finding by independent researcher 6r1ff1n, reported through DuckDuckGo's HackerOne program (report #3619287 — full disclosure: https://hackerone.com/reports/3619287). What follows is my own breakdown of the technique, the attack chain, and the lessons for hunters and defenders.
Analysis by Faizan Akhtar.
The Discovery at a Glance
In early 2026, 6r1ff1n was reviewing DuckDuckGo's public GitHub repositories when one workflow file stood out: .github/workflows/semver-label.yml in duckduckgo/content-scope-scripts — described in its own README as "shared JavaScript used within ALL DuckDuckGo browsers": Android, iOS, macOS, Windows, the Chrome and Firefox extensions, and autofill. Tens of millions of users.
The workflow's job was mundane — auto-applying semantic-version labels to PRs. Its configuration was not: it triggered on pull_request_target with no access controls, no fork detection, and no approval gate, then checked out the PR's code and executed it. Any GitHub user could open a fork PR and get arbitrary code execution on DuckDuckGo's CI runner, with the workflow's secrets in the environment.
The report contained four related findings, including secret exposure via sibling workflows and ten workflows across the org using the same trigger. But the headline — RCE plus a direct road into the release pipeline — is what makes this a case study.
Background: Why pull_request_target Is a Loaded Gun
GitHub has two pull request triggers, and confusing them is one of the most common CI/CD security mistakes in existence. The pull_request trigger runs in the context of the fork — the attacker's copy — with no access to the base repository's secrets. Safe by default.
The pull_request_target trigger runs in the context of the base repository, which means it does get secrets. It exists for workflows that need secrets while handling PRs, like posting comments or applying labels. GitHub's own documented rule: never check out and execute untrusted code under this trigger, because the code comes from the attacker while the secrets come from you.
The classic exploitation pattern even has a name: the "pwn request." Attacker forks the repo, plants malicious code, opens a PR, and the privileged workflow obediently runs it. DuckDuckGo's workflow was a textbook instance — except the blast radius was extraordinary.
The Vulnerable Workflow
The semver-label.yml workflow fired automatically on pull request events — opened, synchronized, reopened — with no conditions: no fork check, no required maintainer label, no author verification.
The fatal step was the checkout: instead of the trusted base branch, the workflow checked out the PR's merge ref — the attacker's fork code merged with the base — then ran dependency installation and a build inside that directory. In Node.js, that is game over by itself: package installation executes lifecycle scripts from the checked-out code. Whoever controls package.json controls the machine.
The runner operated with the base repository's privileges, with sensitive values in its environment — including an Anthropic API key and the automatic GITHUB_TOKEN, which carried pull-requests: write permission.
The Attack Chain, Step by Step
The full chain, reconstructed from the disclosed report:
Step 1 — Fork and plant. The attacker forks content-scope-scripts and edits package.json to add a lifecycle script (auto-executed by npm during installation). A single file, a single commit.
Step 2 — Open a pull request. Any trivial change justifies the PR. The pull_request_target trigger fires immediately and automatically. No maintainer approval, no label, no review needed.
Step 3 — The runner checks out attacker code. The workflow checks out the PR merge ref, pulling the attacker's modified files onto the runner.
Step 4 — Installation executes the payload. When the workflow runs the install step, npm executes the planted script. Attacker commands now run on the runner with the workflow's full environment — including every configured secret.
Step 5 — Secret exfiltration. The script reads secrets from the environment and sends them to attacker-controlled infrastructure. Two prizes: the Anthropic API key and the GITHUB_TOKEN.
Step 6 — From RCE to supply chain. This is where the finding graduates from "CI compromise" to "supply chain catastrophe." The stolen token could write pull-request metadata, so the attacker applies a semantic-version label to their own PR. On merge, the automated release pipeline — driven by those very labels — cuts a new release, producing artifacts with no code signing and no integrity verification.
Step 7 — Poisoned fleet. Attacker-controlled JavaScript ships to every platform DuckDuckGo supports. The researcher later validated the finding with a safe, minimal live proof-of-concept — a non-destructive callback received from a GitHub Actions runner on the same day DuckDuckGo pushed its fix — without exfiltrating any real secrets. But the pipeline path was real, the permissions were real, and with no signing there was no safety net.
An alternative path noted in the report: instead of exfiltrating during installation, the payload could have modified the trusted analysis script the workflow runs afterward — persisting the attack into the build phase.
Why This One Deserves Study
Three things elevate this above a routine misconfiguration report.
First, the blast radius math. Most CI RCEs end at "attacker gets a cloud runner and some secrets." Here, the runner was a stepping stone into an automated release pipeline for code embedded in tens of millions of browsers. The vulnerability's severity is a function of what the pipeline publishes, not just what the runner touches.
Second, the missing safety nets compounded. Unsigned releases. Broad token permissions. Ten workflows sharing the dangerous trigger across the organization. Each a small, defensible-in-isolation decision; together, a chain with every link already open.
Third, the fix was one line. A fork-detection condition on the job — only running when the PR's head repository matches the base — eliminates the entire attack class while preserving function for internal PRs. Thirty seconds of configuration versus a potential compromise of an entire browser fleet.
Remediation: Defense in Depth for pull_request_target
Any one of these mitigations breaks the chain; mature organizations apply several:
-
Prefer the
pull_requesttrigger when the workflow does not strictly need secrets. No secrets, no exfiltration target. - Add an explicit fork check before any checkout of PR code.
- Gate on a maintainer-applied label ("safe to test"), ensuring a human reviews code before the privileged workflow runs it — and ensure the label check itself can't be manipulated by the PR author.
- Use GitHub Environments with required reviewers for secret-handling jobs.
- Minimize token permissions — this token never needed pull-request write access.
- Sign releases and verify signatures downstream, so a compromised pipeline can't silently ship tampered artifacts.
Hunting Methodology: Finding This Bug Class Yourself
The methodology here is cheap, fast, and repeatable — the most valuable part for working hunters:
-
Enumerate workflow files. For any target with a public GitHub organization, list every
.github/workflows/*.ymlfile across every repository. This is pure reconnaissance — no interaction with the target's infrastructure. -
Grep for the dangerous trigger. Search for
pull_request_target(and its cousinworkflow_run, which has similar trust implications). Most organizations have a handful; each is a candidate. -
Read what gets checked out. The critical question: does the workflow check out the PR head, the merge ref, or any attacker-influenced ref? A checkout of
${{ github.event.pull_request.head.sha }}or the merge ref under a privileged trigger is the core signal. -
Check what executes. Look for package manager installs, build scripts, test runners, or any step that runs code from the checkout. In Node.js repos,
npm installalone is sufficient. - Map the secrets. Which secrets are present in the job's environment? What permissions does the token carry? The finding's severity scales with both.
- Trace the downstream. Does the workflow feed a release pipeline, publish packages, or deploy? A CI bug that ends at the runner is a finding; one that ends at the release artifact is a headline.
The researcher also demonstrated tradecraft worth emulating: careful static analysis, a safe non-destructive live PoC (a minimal callback proving code execution on the runner, no secrets touched), contrast with a correctly-guarded workflow elsewhere, and concrete remediations. That is the anatomy of a report that gets taken seriously.
Key Takeaways
-
pull_request_targetplus checkout of PR code plus execution equals remote code execution. This pattern is years old and still everywhere. - Assess CI findings by what the pipeline publishes, not just what the runner exposes. Supply-chain impact is where critical severity lives.
- Unsigned release artifacts turn a CI compromise into a fleet compromise. Signing is the cheapest catastrophic-failure insurance in software distribution.
- The fix is often one line — but only if someone is reading the workflow files. Continuous, boring static review of CI configuration catches what scanners miss.
- For hunters: workflow enumeration is high-signal, low-noise reconnaissance. Add it to your standard checklist for any target with public repositories.
Credit
Full credit for the discovery goes to 6r1ff1n (HackerOne: https://hackerone.com/6r1ff1n), who reported this through DuckDuckGo's bounty program. Original disclosed report: https://hackerone.com/reports/3619287. This post is my own independent analysis of the technique and its lessons.
Report by Faizan Akhtar — The Cyber Security Researcher
Top comments (0)