Skip to main content
Security9 min read

Pull Requests Are Now a Supply-Chain Attack Surface: Lessons from the wfx-loot GitHub Campaign

The wfx-loot campaign shows why pull requests, contributor accounts, and CI automation must be treated as part of the software supply chain. Engineering teams should harden GitHub workflows by tightening permissions, reducing token scope, protecting secrets, and making repository maintenance a recurring DevSecOps discipline.

A pull request used to feel like a collaboration boundary: review the diff, run the checks, merge when green. The wfx-loot campaign is a reminder that pull requests are also an attack surface—one that can connect compromised identities, automated CI workflows, repository permissions, and cloud credentials in a single chain.

According to BleepingComputer, the campaign used 4,837 compromised GitHub accounts to probe 61,764 repositories, open pull requests, and push secrets-stealing malware. For developers, engineering leaders, and CTOs, the lesson is clear: dependency risk is only one part of software supply-chain security. The workflow around code contribution needs the same level of maintenance, modernization, and control.

Context: What the wfx-loot Campaign Changed

Pull Requests Are Now a Supply-Chain Attack Surface: Lessons from the wfx-loot GitHub Campaign
Pull Requests Are Now a Supply-Chain Attack Surface: Lessons from the wfx-loot GitHub Campaign

BleepingComputer reported that threat actors behind a campaign dubbed wfx-loot compromised thousands of GitHub accounts and used them to interact with public repositories at scale. The attackers did not simply publish a malicious package and wait for adoption. They used trusted-looking GitHub identities to probe repositories, open pull requests, and attempt to introduce code designed to steal secrets and cloud credentials.

That detail matters. Many organizations have invested in dependency scanning, software composition analysis, and vulnerability remediation. Those controls are important, but they often focus on what comes into the codebase through packages. The wfx-loot activity highlights another path: attackers can come through the contribution workflow itself.

A pull request can trigger automated tests, build jobs, preview deployments, linting, code generation, or infrastructure validation. Depending on the repository configuration, those workflows may receive tokens, secrets, package registry credentials, or cloud access. If the workflow is too permissive, a malicious pull request can become more than a bad diff—it can become a credential theft opportunity.

This is why repository hygiene belongs in routine platform maintenance. Branch protection rules, CI trigger policies, GitHub token permissions, secret exposure controls, and contributor trust models are not set-and-forget configuration. They are operational controls that age as teams, repositories, and automation patterns evolve.

Main Analysis: Pull Requests as a Supply-Chain Boundary

Contributor Identity Is Part of Your Trust Model

Most teams think about identity in production systems: SSO, MFA, least privilege, offboarding, and audit logging. But developer identity deserves the same rigor. GitHub accounts are not just user profiles; they are signing authorities, collaboration identities, and sometimes automation entry points.

The wfx-loot campaign’s use of 4,837 compromised accounts demonstrates the scale problem. A pull request from an account with history, stars, or prior contributions may look less suspicious than one from a brand-new user. But account age and activity are not sufficient trust signals.

Engineering teams should consider contributor identity as a dynamic signal, not a binary state. Is the account a member of the organization? Is the pull request from a fork? Is this a first-time contributor? Did the account recently change behavior? Is the requested change touching CI, deployment scripts, dependency manifests, or credential-handling code?

For open source maintainers, this does not mean rejecting outside contributions. It means routing higher-risk contributions through safer workflows and limiting what automation can access before human review.

CI Triggers Can Turn a PR into an Execution Surface

Continuous integration is one of the most powerful parts of modern software delivery—and one of the most sensitive. A pull request may trigger scripts that execute attacker-controlled code. If those scripts run with broad permissions or access to secrets, a malicious contributor can attempt to exfiltrate tokens, environment variables, or temporary cloud credentials.

GitHub Actions provides safer patterns, but teams need to use them intentionally. For example, workflows triggered by pull_request behave differently from pull_request_target. The latter can be useful, but it is also dangerous when it checks out and executes untrusted code because it runs in the context of the base repository.

This is where routine maintenance matters. A workflow that was safe when it only ran tests may become risky after someone adds a deployment step, a cloud integration, or a package publishing job. CI files should be reviewed like production infrastructure code, not treated as incidental YAML.

Repository Permissions Often Drift Over Time

Permissions drift is one of the most common maintenance failures in software organizations. A team grants write access during a launch, expands token permissions to unblock a release, or adds a broad secret to simplify a build. Months later, those exceptions become the new baseline.

The wfx-loot campaign is a strong argument for periodic repository permission reviews. Who can push to protected branches? Who can approve pull requests? Which GitHub Apps have access? Which workflow tokens can write contents, packages, pull requests, issues, or deployments? Are default workflow permissions read-only or write-enabled?

For CTOs and engineering leaders, the key is to treat repository permissions as platform configuration. They should be inventoried, reviewed, and governed with the same discipline as cloud IAM policies.

Secrets Exposure Is Still the Blast Radius Multiplier

The campaign’s reported objective—stealing cloud credentials—points to the largest risk in many engineering environments. Source repositories and CI systems often sit close to powerful credentials: deployment tokens, package registry keys, cloud access keys, database URLs, and service account secrets.

If a malicious pull request can cause a workflow to print environment variables, run a modified build script, upload artifacts, or call an external endpoint, secrets may be exposed. Even short-lived credentials can be dangerous if they are overly broad or if detection is slow.

Secret scanning is necessary, but it is not enough. Teams need to reduce the number of long-lived secrets in CI, prefer short-lived federated identity where possible, and enforce least privilege on cloud roles. The goal is not only to find leaked secrets—it is to make leaked secrets less useful.

Practical Implications for Engineering Teams

1. Revisit Branch Protection and Merge Controls

Start with the repositories that matter most: production services, infrastructure-as-code, shared libraries, deployment tooling, and public open source projects. Confirm that protected branches require pull request review, status checks, and up-to-date branches before merge. Require code owner review for sensitive paths such as .github/workflows, deployment scripts, package manifests, Dockerfiles, and infrastructure directories.

For high-risk repositories, consider requiring signed commits or verified commits, though these controls should complement—not replace—review and CI hardening.

2. Harden Pull Request Workflow Rules

Review every GitHub Actions workflow that runs on pull requests. Identify which workflows execute untrusted code, which workflows receive secrets, and which workflows have write permissions.

As a practical rule, workflows triggered by untrusted pull requests should run with minimal permissions and no access to sensitive secrets. If a workflow needs privileged access, separate it from untrusted code execution and require a maintainer-controlled approval step.

Also review the use of pull_request_target carefully. It can be appropriate for labeling, commenting, or policy checks, but it should not execute code from a fork without strict controls.

3. Reduce GITHUB_TOKEN and App Permissions

GitHub’s default token permissions should be set to read-only where possible, then elevated per workflow only when required. Avoid broad write permissions across all workflows. If a job only needs to read repository contents, it should not be able to write packages, create releases, or modify pull requests.

The same principle applies to GitHub Apps, deploy keys, and service accounts. Remove stale integrations, rotate credentials, and document why each integration exists.

4. Modernize Cloud Credential Hygiene

If your CI system still relies on long-lived cloud access keys, prioritize modernization. Use OIDC-based federation from GitHub Actions to cloud providers where possible, with tightly scoped roles and conditions. Restrict roles by repository, branch, environment, and workflow identity.

Short-lived credentials are not a complete defense, but they reduce the window of abuse. Pair them with monitoring for unusual role assumptions, access from unexpected geographies, and abnormal API activity.

5. Treat Repositories as Software Inventory

Sonatype has written about the idea of looking beyond storage and treating repositories as software inventory. That framing is useful here. A repository is not only where code lives; it is a map of dependencies, build logic, release processes, permissions, owners, and operational risk.

For platform teams, this means maintaining an accurate inventory of repositories, owners, criticality, workflow types, exposed secrets, and deployment paths. Without inventory, security hardening becomes reactive and inconsistent.

6. Add PR Security Checks to Routine Maintenance

Security work is often framed as incident response or compliance. The better approach is maintenance. Add repository hardening to your recurring platform maintenance calendar. Quarterly reviews can include:

  • Branch protection and code owner validation
  • GitHub Actions permission review
  • Secret scanning and credential rotation
  • Stale collaborator and app removal
  • Review of public repositories and fork settings
  • Audit of workflows that touch cloud credentials
  • Testing of alerting for suspicious CI behavior

This is especially important during modernization projects. When teams migrate CI systems, consolidate repositories, update build pipelines, or move deployments to cloud-native platforms, they should also re-evaluate trust boundaries.

What CTOs Should Ask This Week

For engineering executives, the wfx-loot campaign should prompt a few direct questions:

  • Which repositories can deploy to production or access cloud credentials?
  • Do pull requests from forks ever run with secrets?
  • Are GitHub Actions tokens read-only by default?
  • Who owns each critical repository and workflow?
  • Can a first-time contributor trigger privileged automation?
  • How quickly would we detect credential exfiltration from CI?

These questions are not about slowing teams down. They are about making developer velocity sustainable. A fast delivery pipeline that leaks cloud credentials is not modern—it is fragile.

Conclusion: Secure the Workflow, Not Just the Code

The wfx-loot campaign is a reminder that software supply-chain security now includes the human and automation paths around the repository. Attackers are not limited to malicious dependencies; they can abuse compromised contributor identities, permissive pull request workflows, and over-scoped CI credentials.

For development teams, the response should be practical and continuous: maintain repository inventory, harden CI triggers, minimize token permissions, protect secrets, and review branch controls as part of normal platform upkeep. The next phase of software modernization is not only about upgrading frameworks and infrastructure. It is about modernizing the trust model that moves code from pull request to production.

Vibgrate CLI

See a real scan run

A replay of the actual CLI running against our test repositories — live progress, real findings, a genuine DriftScore. Nothing executes in your browser.

Replay
demo@vibgrate — bash
❯ npx @vibgrate/cli scan
 
╭──────────────────────────────────────────╮
│ Vibgrate Drift Report │
╰──────────────────────────────────────────╯
 
── node-turborepo (node) .
Runtime: >=18.0.0 (8 majors behind)
Frameworks:
Turbo: 1.13.4 → 2.11.11 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 1 1-behind 3 2+ behind 1 unknown
 
── @repo/admin (node) apps/admin
Frameworks:
TanStack Query: 5.107.1 → 5.107.1 (current)
React: 18.3.1 → 19.3.1 (1 behind)
React DOM: 18.3.1 → 19.3.1 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vite: 5.4.21 → 8.3.8 (3 behind)
Dependencies:
3 current 9 1-behind 3 2+ behind 4 unknown
 
── @repo/api (node) apps/api
Frameworks:
Express: 4.22.3 → 5.3.0 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 5.1.0 (4 behind)
Dependencies:
7 current 4 1-behind 4 2+ behind 4 unknown
 
── @repo/web (node) apps/web
Frameworks:
Next.js: 14.2.35 → 16.4.6 (2 behind)
React: 18.3.1 → 19.3.1 (1 behind)
React DOM: 18.3.1 → 19.3.1 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 6 1-behind 3 2+ behind 5 unknown
 
── @repo/config (node) packages/config
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 2 1-behind 5 2+ behind 0 unknown
 
── @repo/database (node) packages/database
Frameworks:
Prisma: 5.22.0 → 7.10.1 (2 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 0 1-behind 3 2+ behind 1 unknown
 
── @repo/types (node) packages/types
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
0 current 0 1-behind 1 2+ behind 1 unknown
 
── @repo/ui (node) packages/ui
Frameworks:
React: 18.3.1 → 19.3.1 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
React: 18.3.1 → 19.3.1 (1 behind)
Dependencies:
1 current 4 1-behind 1 2+ behind 1 unknown
 
── @repo/utils (node) packages/utils
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 5.1.0 (4 behind)
Dependencies:
0 current 1 1-behind 2 2+ behind 1 unknown
 
Tech Stack
Frontend: React, React DOM
Meta-frameworks: Next.js
Bundlers: tsx, Turbo, Vite
CSS / UI: Autoprefixer, PostCSS, Tailwind CSS
Backend: Express
ORM / Database: Prisma, Prisma Client
Testing: Vitest
Lint & Format: ESLint, ESLint Prettier, ESLint React, Prettier, typescript-eslint
 
Services & Integrations
Auth: JWT 9.0.3
Databases: Prisma 5.22.0
 
TypeScript
v5.3.3 · strict ✔ · MIXED · target: ES2022
 
Build & Deploy
Package Managers: pnpm
Monorepo: npm-workspaces, pnpm-workspaces, turbo
 
Product Purpose Signals
Frameworks: react, nextjs
Evidence: 177
Top Signals:
- [heading] Dashboard (apps/admin/src/pages/Dashboard.tsx)
- [title] Revenue Overview (apps/admin/src/pages/Dashboard.tsx)
- [copy] workspace:* (packages/ui/package.json)
- [copy] ./dist (packages/ui/tsconfig.json)
- [copy] ./src/index.ts (packages/ui/package.json)
- [copy] @repo/config/tsconfig-base.json (packages/ui/tsconfig.json)
- [copy] @repo/ui (packages/ui/package.json)
- [copy] #3b82f6 (apps/admin/src/pages/Dashboard.tsx)
Unknowns:
- No pricing or billing evidence found.
- No integrations/connectors evidence found.
- No route structure evidence found.
 
Security Posture
Lockfile ✖ · .env ✔ · node_modules ✔
 
Platform
Native modules: turbo
 
Code Quality
Files: 36 · Functions: 183 · Avg complexity: 2.62 · Avg length: 21.13 lines
Max nesting: 2 · Circular deps: 0 · Dead code: 0%
God files: apps/admin/src/pages/Products (448 lines)
 
Database Schema
postgresql · 8 models · 1 enum
Models: Address, CartItem, Category, Order, OrderItem (+3 more)
 
Findings (16 errors, 11 warnings)
✖ @types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.6.6).
vibgrate/dependency-major-lag in .
✖ 60% of dependencies are 2+ major versions behind in node-turborepo.
vibgrate/dependency-rot in .
✖ Node.js runtime ">=18.0.0" reached end-of-life on 2025-04-30 (latest: 26.0.0).
vibgrate/runtime-eol in .
✖ vite is 3 major versions behind (spec: ^5.0.12, latest: 8.3.8).
vibgrate/dependency-major-lag in apps/admin
✖ Vite is 3 major versions behind (current: 5.4.21, latest: 8.3.8).
vibgrate/framework-major-lag in apps/admin
✖ @types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.6.6).
vibgrate/dependency-major-lag in apps/api
✖ vitest is 4 major versions behind (spec: ^1.2.1, latest: 5.1.0).
vibgrate/dependency-major-lag in apps/api
✖ Vitest is 4 major versions behind (current: 1.6.1, latest: 5.1.0).
vibgrate/framework-major-lag in apps/api
✖ @types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.6.6).
vibgrate/dependency-major-lag in apps/web
✖ eslint-plugin-react-hooks is 3 major versions behind (spec: ^4.6.0, latest: 7.1.1).
vibgrate/dependency-major-lag in packages/config
✖ 56% of dependencies are 2+ major versions behind in @repo/config.
vibgrate/dependency-rot in packages/config
✖ 75% of dependencies are 2+ major versions behind in @repo/database.
vibgrate/dependency-rot in packages/database
✖ 100% of dependencies are 2+ major versions behind in @repo/types.
vibgrate/dependency-rot in packages/types
✖ vitest is 4 major versions behind (spec: ^1.2.1, latest: 5.1.0).
vibgrate/dependency-major-lag in packages/utils
✖ 67% of dependencies are 2+ major versions behind in @repo/utils.
vibgrate/dependency-rot in packages/utils
✖ Vitest is 4 major versions behind (current: 1.6.1, latest: 5.1.0).
vibgrate/framework-major-lag in packages/utils
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in .
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/admin
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/api
⚠ Next.js is 2 major versions behind (current: 14.2.35, latest: 16.4.6).
vibgrate/framework-major-lag in apps/web
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/web
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/config
⚠ Prisma is 2 major versions behind (current: 5.22.0, latest: 7.10.1).
vibgrate/framework-major-lag in packages/database
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/database
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/types
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/ui
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/utils
 
╭──────────────────────────────────────────╮
│ Top Priority Actions │
╰──────────────────────────────────────────╯
 
1. Upgrade EOL runtime in node-turborepo
End-of-life runtimes no longer receive security patches and block ecosystem upgrades.
./.
>=18.0.0 → 26.0.0 (8 majors behind)
Impact: −10 drift points (runtime & EOL)
 
2. Fix security posture: no lockfile found
Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.
./
Missing: package-lock.json, pnpm-lock.yaml, or yarn.lock
 
3. Upgrade Vitest 1.6.1 → 5.1.0 in @repo/api (+2 more)
4 major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.
./apps/api
Vitest: 1.6.1 → 5.1.0 (4 majors behind)
./packages/utils
Vitest: 1.6.1 → 5.1.0 (4 majors behind)
./apps/admin
Vite: 5.4.21 → 8.3.8 (3 majors behind)
Impact: −5–15 drift points
 
4. Reduce dependency rot in @repo/types (100% severely outdated)
1 of 1 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/types
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
5. Reduce dependency rot in @repo/database (75% severely outdated)
3 of 4 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/database
@prisma/client: 5.22.0 → 7.10.1 (2 majors behind)
prisma: 5.22.0 → 7.10.1 (2 majors behind)
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
╭──────────────────────────────────────────╮
│ Architecture Layers │
╰──────────────────────────────────────────╯
 
Archetype: nextjs (80% confidence)
Files classified: 24 (11 unclassified)
Folders classified: 8
apps/admin/src presentation 100% 4 files
apps/admin/src/pages presentation 100% 2 files
apps/api/src/middleware middleware 100% 2 files
apps/api/src/routes routing 100% 2 files
apps/web/src/app presentation 100% 4 files
apps/web/src/app/products presentation 100% 2 files
apps/web/src/app/products/[id] presentation 100% 1 file
packages/ui/src presentation 100% 6 files
Unclassified source (sample): 11
 
presentation 15 files drift ████████████████████ 100 risk high
routing 4 files drift ████████████████████ 100 risk high
middleware 2 files drift ███████▍░░░░░░░░░░░░ 37 risk moderate
config 2 files drift ░░░░░░░░░░░░░░░░░░░░ 0 risk none
shared 1 file drift ████████████████████ 100 risk high
 
╭──────────────────────────────────────────╮
│ DriftScore Summary │
╰──────────────────────────────────────────╯
 
DriftScore: 70/100
Risk Level: HIGH
Projects: 9
Classified: 8 nano · 1 micro · 0 small · 0 standard
Billable: 0.42 · 9 detected → 0.42 billable projects (micro-project pricing)
0.1 micro · 0.32 nano
These fractions add up across repositories, then round down to whole billable projects.
 
Score Breakdown
Runtime: ████████████████████ 100
Frameworks: ███████████▊░░░░░░░░ 59
Dependencies: ███████░░░░░░░░░░░░░ 35
EOL Risk: ████████████████████ 100
 
Scanned at 2026-11-12T23:47:37.520Z · 5.4s · 286 files scanned · 56 workspace files · 27 dirs
❯
Press Run to start.