---
title: Security Incident Report — 31 May 2026 — Force-Push Supply-Chain Backdoor
---

# 🛡️ Security Incident Report

A stolen GitHub credential was used to force-push credential-stealing malware
across the organisation on 31 May 2026. It was never seen running, and there is no evidence of
customer, patient, or production impact. Here is what happened, why it stayed contained, and how to
spot the scam that started it.

Status: CONTAINED

[Full investigation methodology & per-thread analysis →](/security/incidents/2026-05-31/analysis/)

## 📊 At a glance

~7 min

total attack window

180

force-pushes, 1 stolen login

15

repos hit (of ~87 scanned)

0

executions seen in CI / builds

0

evidence of customer / patient / prod impact

#### 📌 How to read this report

The malicious commits were stamped with the names and dates of real teammates.
Those names are **forged** — git lets anyone write any value into a commit's "author"
field. Where a real person is named here it is to record that their identity was *impersonated*,
or that they are a *victim*. **Nothing in this report attributes fault to any
individual.** The only actor we can speak about with certainty is one stolen credential; the
human operating it is external and unidentified.

## 🕐 How it unfolded

Four phases: the **lure** (months before), the
**attack** (7 minutes on 31 May), the
**defences** that held, and the
**response**.

🎣

6 Feb 2026

#### The lure lands

**Nikhil** receives an Upwork "Invitation to Interview" for a *"Python Machine
Learning Engineer (Senior/Staff)"* role described as a *"long-term crypto trading prediction
project,"* from a user called **"Princes A."** An ML-plus-crypto freelance
approach is the signature opening of the DPRK-linked "Contagious Interview" campaign.

⚠️

27 Mar 2026 — last contact in evidence

#### Upwork itself warns him

Upwork emails Nikhil: *"You may have received a malicious file or link… another Upwork user
may have shared malware with you,"* naming the same actor (**"Princes Ampalayo"**).
He runs a virus scan and moves his passwords to a password manager — the reasonable response. But
this campaign's payload **silently steals invisible login material** (browser sessions,
OAuth tokens) that a scan can't undo and that no one would know to revoke. The theft happened on a
**personal device, off N1's systems** — outside what N1 can control; the login stayed
usable until N1 made logins like it unusable.
*(This is the most recent email in the evidence; there is no later contact.)*

🔓

Feb – May 2026 (inferred)

#### A credential is stolen and sits unused

The most consistent explanation for everything that follows: the scam stole one of Nikhil's
pre-existing GitHub credentials off his personal surface. **We cannot prove the exact
credential or the moment of theft** — but because nothing structurally invalidated it, a
credential taken in this window would still have been valid weeks later. The screenshots prove he was
*solicited* and *warned*; they do **not** prove he downloaded or ran anything.

💥

31 May 2026 · 13:52:45Z

#### The burst begins

An external operator replays the stolen `numberyy` login **remotely**
(not from Nikhil's machine) and starts force-pushing. No new token is created and no sign-in event
appears — consistent with replaying a credential that already existed.

🗜️

13:52 – 13:59Z · ~7 minutes

#### 180 force-pushes, 15 repos, two malware families

An automated script plants **two related credential-stealing payloads** into
build/config files (ESLint, Vite, Playwright, PostCSS, Eleventy…), spraying them across every
branch it can reach. It forges **~20 teammates' identities** and back-dates the commits
to look like ordinary history, burying the payload under whitespace-only line-ending churn.

🛡️

During the burst — automatic

#### Branch protection holds the line

On every repo with a **protected default branch**, the force-push is
**rejected at the mainline** — the malware is pushed only onto dormant feature/PR
branches. Just **3 repos** had unprotected defaults the attacker reached directly.
This is the single reason the blast radius stayed small.

👀

Discovery

#### Zabir catches it

**Zabir** — whose name was itself forged onto malicious commits — notices a poisoned
branch during a routine `git pull --rebase`, recognises the malware, and isolates it in a
clean branch. A human caught what the tooling did not. He is a **victim and the first
responder**, not a culprit.

🔒

Response

#### Locked down and closed at the protocol level

The account was blocked and removed; the org was set to read-only; reachable poisoned branches
were cleaned; malicious workflows disabled; and — the control that actually answers this incident —
**signed commits are now required on every branch in every repo**. A stolen token alone
can no longer land a commit (verified live: an unsigned push is rejected even for an owner).

🔁

Now

#### Cleanup complete; hardening under way

All poisoned branches removed (feature and default) and the affected default branches cleaned;
staging + production clusters and the full Cloudflare account scanned **clean**. The
Prevention + Detection hardening program is now the active work.

## ✅ Were we harmed? No — and here is how we know

We did not assume we were fine. We checked the evidence.

never ran

#### The malware never executed

It only does damage when built or executed. Every post-attack run was already disabled, died at
dependency install **because the attacker's own code was incomplete (luck, not a safeguard)**,
or ran a build step that never loads the poisoned file.

login system

#### Authentication was untouched

Across the window and the days after, sign-ins behaved completely normally — no anomalies.

our cloud

#### Nothing stolen from infrastructure

We don't store long-lived keys in the cloud to steal, and records show no attacker set up any new
access. Clusters and Cloudflare scanned clean.

no second attacker

#### One credential, no hidden tenant

Only the one stolen login pushed any payload; every other push in the window was ordinary
developer work. No malicious dependency in our code; all internal artifacts verified untampered.

## 🦠 The malware

Two payload families arrived in the same burst, both hidden in files that run as a side effect of
normal development, both pointing at the same external infrastructure.

Family A

#### "PolinRider" — obfuscated blob

A giant single-line obfuscated blob appended to config files (`eslint.config.js`,
`vite.config.ts`, etc.). It smuggles Node's `require` into an ES module and runs
code via the `Function` constructor (an `eval` that dodges lint rules).

Family B

#### The eval-loader

A small snippet that base64-decodes a hidden URL, fetches it, and `eval`s whatever comes
back. **It carries none of Family A's markers** — so a marker-only scan missed it
entirely. That is why broad, behaviour-based detection is mandatory.

### What it does, once it runs

Both families converge on the same on-chain payload (Family A inlines it; Family B fetches it from
the C2). We decoded that loader and recovered its stages in an isolated sandbox. The C2 was contacted
exactly once — a **single manual request that captured, not ran, the payload** — and the
stealer's own command-and-control socket was **never contacted, so nothing was exfiltrated**.
It is a three-stage "EtherHiding" chain that hides its later stages inside blockchain transactions:

Stage 1

#### Downloader

Runs in the build process; pulls its
next stage out of a blockchain transaction. Reads no credentials itself.

→

Stage 2

#### Second loader

Fetches and decrypts the next
stage from another on-chain transaction.

→

Stage 3

#### Info-stealer (RAT)

An
[InvisibleFerret-class](https://attack.mitre.org/software/S1245/)
remote-access trojan: fingerprints the host and waits on a remote socket. **Never reached**
in our sandbox.

#### Why this was not aimed at N1 specifically

The same command-and-control string appears in unrelated public repositories that
were force-pushed the same day. This was an **automated, multi-victim sweep** that hit many
organisations at once — not infrastructure built for us. The toolchain matches the publicly documented
DPRK-linked ["Contagious Interview"](https://attack.mitre.org/groups/G1052/)
campaign (an external attribution we did not independently confirm).

## 🎯 Blast radius — and the one thing that contained it

~70

repos fully clean

15

repos carried payload

173

branch tips (mostly duplicates)

3

unprotected default branches hit

The big branch-tip number is mostly **replication** — the attacker sprayed the same file
onto every branch, so one poisoned config shows up across twenty-odd tips rather than being twenty
separate compromises. (173 tips = 170 feature branches + 3 default branches; of ~87 repos scanned,
70 were clean, 2 empty, 15 hit.)

#### 🛡️ Branch protection decided what got hit

On repos with a protected default branch, the force-push was rejected at the
mainline and the malware was confined to dormant side-branches. Only three repos — whose defaults were
**unprotected** — were reached directly; **all three have since been cleaned**.
Org-wide hardening so that *every* repo's default is protected is the headline follow-up.

## 🧑‍🤝‍🧑 The people named — all victims or impersonated

Because the attacker forged ~20 identities, real names appear in the malicious history. To be
unambiguous:

N

Victim — credential stolen

#### Nikhil

His personal GitHub credential was stolen off-platform (on a personal device, off N1's systems)
and replayed remotely. His work machine and entire Claude Code archive (2,260 sessions) were
examined and are clean — the malware never transited N1's surface.

Z

Victim & first responder

#### Zabir

His identity was forged onto malicious commits, yet he is the one who **caught and contained
it** during a routine rebase. The hero of the story, not a suspect.

S

Impersonated only

#### Syamil

His name was used as a forged label, but he made **no pushes at all** in the entire
dataset. Purely impersonated.

J

Impersonated · co-investigator

#### Jasper & others (Puneet, Paawan, and ~16 more)

Names copied into forged commit metadata. Jasper helped investigate; cryptographic signatures prove
the overwhelming majority of his real commits are genuine and clean. A forged author name is not a
record of who did anything.

## 🎣 How to spot a campaign like this — everyone, please read

This incident did not begin with a clever exploit. **It began with a job offer.**
The single most valuable habit the whole team can build is recognising the lure before interacting with it.

### Red flags in the approach

💼

#### Unsolicited recruiter DM

On Upwork, LinkedIn, Telegram, or email — for a **senior/staff dev or ML/AI role.**

🪙

#### Crypto / trading / web3 framing

A "software" role tied to crypto trading or web3 is the campaign's signature. (Ours was a
"crypto trading prediction project.")

💻

#### "Just run our code"

A take-home task, a demo repo to clone and `npm install`, an SDK or "meeting app" to
install. **The install step is the attack.**

📎

#### Attachments & external links

Files or links inside a recruiting message; a push to move to WhatsApp/Telegram; urgency or
flattery. Sender handles that feel slightly off ("Princes A." / "Princes Ampalayo").

#### 🚫 Before you interact

- **Never run interview / take-home / "candidate" code on your work machine** — or any
  machine that holds logins. If you must run it, use a disposable VM with no credentials.
- **Don't `npm install` / `pip install` an unknown repo "just to see."**
- **Report it to Arun right away** (and report the user on the platform) — even if
  you didn't run anything. Early reporting shrinks the blast radius.

#### ⚠️ If you think you already interacted with one

- **Tell Arun immediately** — even if you've already run a virus scan. A silent stealer
  takes login material you can't see (session cookies, OAuth tokens), so only revoking your GitHub
  sessions reliably shuts it off; a scan does not.
- Then rotate what you can — GitHub tokens (PATs *and* the OAuth logins behind `gh`
  / Git Credential Manager), SSH keys, cloud keys — but fast session revocation is the real safety
  net, not personal cleanup.

### Two lessons that generalise

#### 🔑 A stolen credential must be made useless by design

You can't rely on catching a silent, signal-less theft, and 2FA doesn't stop a replayed token. The
fix is structural: required signed commits (now enforced — a stolen token can't push) plus short-lived
tokens and stopping the theft at the endpoint.

#### ⚙️ Build/config files run code

`eslint.config.js`, `vite.config.ts`, `playwright.config.ts`, a
committed `.env` — these are executable surfaces, not inert text. A surprising diff in one
(a giant one-line blob, or an `atob`/`fetch`/`eval` sequence) is
malware until proven otherwise.

## 🧭 What we've done & what's left

### Done (and verified)

- Attacker account blocked and removed from the org
- Organisation locked to read-only by default
- **Signed commits required on every branch, every repo** — verified live
- Two-factor authentication enforced org-wide
- **All three** poisoned default branches cleaned (developer-portal reset earlier;
  n1-qa + skills-marketplace cleaned forward with signed commits); all poisoned feature branches deleted
- **main and develop** protected (no force-push, no deletion, PR required) on
  every repo — current and future
- Malicious workflows disabled
- Staging + production clusters and full Cloudflare account scanned clean
- **Persistence sweep clean** — no planted webhook, deploy key, or secret at the
  repo or org level
- Vestigial teams removed (writers / maintenance-admin / readonly)
- Immutable evidence backups of all affected repos preserved; credential rotation started

### Immediate close-out

- Finish precautionary credential + CI-secret rotation (Darpan, DevOps); re-grant write per team
- Obtain the account's security log to fully close the entry-vector story
- Ask GitHub Support to purge the orphaned malicious commits

## 🚀 What we're doing next — owned by Arun & Eyad

The root cause is closed. The forward work is a focused, ~2-week, open-source program in two
tracks — a small set of standing capabilities that catch what actually threatened us, not a tooling
estate.

Track A — Prevention

#### One blocking CI check on every repo

A single `security-gate` that chains a **shape-based malware/obfuscation
scanner** (the upgrade that would have caught the loader), dependency-CVE scanning, secret
scanning, a trusted-registry assertion, Actions hardening, and the repo's own tests (with required
failure-mode coverage) — enforced org-wide as a required status check, mirrored by pre-commit
hooks, with commit-signing onboarding so the keystone isn't fought.

Plus closing the live audit findings: the public wheel registry (a seamless developer + CI access
solution is in the works), Cloudflare TLS + token TTLs, ClickUp webhooks, over-broad app grants, and
pinning shared CI by SHA.

Track B — Detection & Response

#### See it in minutes; trip on stolen-credential use

A GitHub org-webhook → Slack alert on the events that mattered (**force-push burst**,
new collaborator, new app, ruleset change); a nightly snapshot-diff of apps/keys/webhooks/members/
rulesets to fill the audit-log gap; an **AWS honey-key tripwire** that fires the instant
a stolen credential is used; and a read-only, PHI-free Slack bot as the one place to ask "is anything
wrong right now." Plus a one-page IR runbook and a lure-awareness note for the team.

Tracked separately, on their own budget (owner
decision): SignOz log PHI-redaction (a HIPAA gap), and endpoint EDR / device imaging.

## 🔎 Indicators of compromise (for security tooling)

Show the full IOC list

### Source-code markers

- PolinRider: `global['!']` (value `'9-0315-1'`), `_$_1e42`,
  `rmcej%otb%`; a `createRequire(import.meta.url)` call plus a single
  >2000-character line in a `*.config.*` file; the `Function` constructor used
  as `eval`.
- eval-loader: `atob(process.env.AUTH_API_KEY)` → `node-fetch` →
  `eval`; committed `.env` setting `AUTH_API_KEY`; log string
  `Auth Error!`.

### Network / host

- C2: `auth-confirm-six.vercel.app`; sibling `ipcheck-six.vercel.app`; the
  `<word>-six.vercel.app` pattern.
- Anomalous from build/CI (legitimate services — alert, don't necessarily block):
  `api.trongrid.io`, `fullnode.mainnet.aptoslabs.com`,
  `bsc-dataseed.binance.org`, `bsc-rpc.publicnode.com`.
- Stage-3 socket C2 on port `1224`.

### Target files

`eslint.config.*`, `postcss.config.mjs`,
`vite.config.*`, `playwright.config.ts`, `server.ts`,
`eleventy.config.js`, `tailwind.config.js`.

### Lure & delivery

Upwork user **"Princes A." / "Princes Ampalayo"**; ML + crypto-trading
freelance pretext. Delivery signature: a burst of force-pushes from one account with
**back-dated author dates and forged identities** — trust the push event in the activity
log, never the commit metadata.

#### Detection lesson

Marker-scanning alone is **insufficient** — it missed the eval-loader
family entirely. Pair marker strings with **behaviour-based** rules (dynamic
decode-and-execute, oversized one-line config blobs, build processes making network calls).

Confidential — Internal Only

N1 Healthcare · Security Incident of 31 May 2026 · Report dated 4 June 2026 · Status: Contained.
This document contains no live malware. Do not share externally.
