Cremit
/incidentsfield log
CatchesCampaignsExfilPatternsLLMIncidentsMethodology
↺rss↗cremit.io

incidents.cremit.io

A reference feed of real-world Non-Human Identity (NHI) credential leak incidents. Maintained by Cremit.

Browse

  • All incidents
  • npm supply chain
  • CI/CD compromise
  • Methodology

Subscribe

  • RSS feed
  • @cremit_io
  • GitHub
// status
monitor active
// build
2026-05-20
// origin
cremit · seoul, kr
// license
CC BY 4.0

© 2026 Cremit. content reuse encouraged with attribution.

home/incidents/antv-mini-shai-hulud-2026
CRITICAL9.4·confirmed·disclosed May 19, 2026·22 min read

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.

↗also on cremit.io/blog

AntV npm account compromise: Mini Shai-Hulud wave, May 2026

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 atool credential: 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 under atool between 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 other MAL-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

Attack flow: two execution paths from one stolen atool npm session to credential exfiltration via t.m-kosche.com

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:

  1. preinstall hook (primary). Modified package.json carries "preinstall": "bun run index.js". A 498 KB obfuscated index.js (SHA-256 a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c, per SafeDep) ships at the tarball root. bun is invoked instead of node for 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.

  2. optionalDependencies git-URL injection (fallback). The compromised package.json also 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 real antvis/G2. Per SafeDep, the attacker forks antvis/G2 (any GitHub account can do this), sets git config user.email to 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 in antvis/G2's object store and remains fetchable by SHA even after the fork is gone, until GitHub eventually garbage-collects unreachable objects. npm install resolves any commit by SHA without checking branch, tag, or fork origin, so no push event appears in antvis/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's prepare script executes the same harvester. The same prepare-script abuse pattern carried the TanStack wave's worm logic; this wave bolts it on as a redundancy layer alongside preinstall.

    SafeDep documents three imposter SHAs in use this wave: 1916faa3…2876569 (626 versions), 7cb42f57…55b3a7a (2 versions), and dc3d62a2…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.json containing a Claude Code SessionStart hook with matcher: "*" and command: "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.json with a task labelled Environment Setup, "runOn": "folderOpen", that calls node .claude/setup.mjs whenever 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 contain setup.mjs or hooks pointing at it
  • Audit developer machines for ~/.claude/package/index.js and ~/.codex/package/index.js
  • Grep every per-project and per-user settings.json for command.*setup\.mjs strings
  • In the very near term, treat any agent / IDE session that runs an unexpected node child 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/credentials and ~/.aws/config, EC2 IMDS at 169.254.169.254/latest/meta-data/iam/security-credentials/, ECS task metadata at 169.254.170.2, and Secrets Manager via the in-region API endpoint.
    • GCP: service account JSON files, gcloud ADC at ~/.config/gcloud/application_default_credentials.json.
    • Azure: ~/.azure/, env-based service principal credentials.
  • 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: .npmrc contents and freshly-minted publish tokens via https://registry.npmjs.org/-/npm/v1/tokens.
    • GitLab CI, Travis CI, CircleCI, Jenkins: token files and env credentials when present.
  • 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_ADDR env + token files at ~/.vault-token.
    • Docker auth: ~/.docker/config.json.
    • Database connection strings from env and common config locations.
  • 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.

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, not t.<random>.com. CI egress controls should treat any traffic to *.m-kosche[.]com as malicious.
  • AWS IMDS at 169.254.169.254 and ECS metadata at 169.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 to Here 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.js SHA-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):

PublisherDistinct package namesNotable downloads
wang121230@antv/g-svg (267k/wk), @antv/g-webgpu (107k/wk)
iaaron26@antv/g6-core (71k/wk), @antv/awards
alex_zjt21@antv/g-plugin-html-renderer (63k/wk), @antv/s2
atool18@antv/g2-extension-plot (120k/wk), timeago.js
lzxue16@antv/l7-component (50k/wk), @antv/g6
newbyvector11@antv/xflow-core (32k/wk), @antv/x6-common
lvisei9@antv/l7-composite-layers
zengyue9@antv/f-charts
panyuqi8@antv/a8, @antv/attr (170k/wk)
jiulingyun6@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):

PackagePublisher field on malicious versionWeekly downloads
@antv/g2moayuisuda354,016
@antv/g-svgwang1212267,116
@antv/algorithmkopiluwaky199,493
@antv/attrpanyuqi170,218
@antv/adjustkasmine144,049
@antv/g2-extension-plotatool120,302
@antv/g2plotsiqishen108,522
@antv/g-webgpuwang1212106,989
@antv/graphinbanxuan81,973
@antv/g6-coreiaaron70,761
size-sensoratool4,200,000/mo
echarts-for-reactatool3,800,000/mo
timeago.jsatool1,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

  1. 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. Use npm token list to enumerate and npm token revoke <token-id> plus a fresh issuance.
  2. Block egress to *.m-kosche[.]com at corporate proxies and CI egress filters. Legitimate code does not POST to that domain.
  3. Pin and review npm dependency upgrades. npm install <name>@latest with 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.
  4. Enforce --ignore-scripts in CI wherever feasible, and additionally block git+ssh:///github: resolvers on the optionalDependencies field, the secondary execution path requires both, but the second one is rare enough in legitimate ecosystems that an outright block costs near-nothing.
  5. Treat IMDSv1 as malware-facing. Require IMDSv2 with HttpPutResponseHopLimit=1 on every EC2 instance; container-side credentials should rely on workload-identity (IRSA, GKE Workload Identity, Azure AD pod identity) rather than instance metadata.
  6. Audit Vault auth methods. Replace any long-lived VAULT_TOKEN-in-env pattern with workload-bound JWT/OIDC auth tied to the runner identity.
  7. Diff dependencies across release windows. The injection moment is the version that adds a preinstall script or a git-URL optionalDependencies entry, every prior version of the same package was clean. npm diff <pkg>@<old> <pkg>@<new> makes the change obvious.
  8. Force a fresh OSSF malicious-packages pull on CI hosts: the MAL-2026-3982 … MAL-2026-4159 advisories were published the same day. CI scanners that don't refresh their advisory database hourly will lag the attack window.

Cremit Analysis

Argus detection pipeline: multi-stage cascade with OSSF MAL-* override

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:

  1. 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 as auto-published disposition with provenance OSV malicious-packages advisory (MAL-*), externally-validated.

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

AxisFiresMember countCombined weekly blast
velocity6170285,677
owner-change-wave:active3301,959,650
publisher:npm:lzxue420102,376
publisher:npm:pddpd4663
publisher:npm:pomelo-nwu3350
publisher:npm:jiulingyun15,
cred-target:ssh-private-keys36295
cred-target:aws-credentials-file2510,847
cred-target:gh-cli-hosts13,

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, now kasmine, previously atool@0.2.3
  • @antv/dom-util, now kasmine, previously atool@2.0.3
  • @antv/event-emitter, now atool, previously zqlu@0.1.2
  • @antv/g-shader-components, now alex_zjt, previously panyuqi@1.8.7
  • @antv/g6-element, now banxuan, previously iaaron@0.8.24
  • @antv/graphin-components, now iaaron, previously pomelo-nwu@2.4.0
  • timeago-react, now domdomegg, previously alanwei0@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)

DimensionValueRationale
Blast radiusecosystem-wide323 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.
ReachabilityproductionThe 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 levelvariableHarvested 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 atool npm 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 publisher audit logs alone, the activity is indistinguishable from legitimate work by atool / 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. [1]
    Mini Shai-Hulud Strikes Again: 317 npm Packages Compromised (SafeDep writeup)
    primary·2026-05-19·safedep.io
  2. [2]
    AntV Packages Compromised (Socket writeup)
    primary·2026-05-19·socket.dev
  3. [3]
    OSV: MAL-2026-3982 (@antv/g6)
    primary·2026-05-19·osv.dev
  4. [4]
    OSV: MAL-2026-4159 (xmorse)
    primary·2026-05-19·osv.dev
  5. [5]
    OSSF malicious-packages: npm/xmorse
    primary·2026-05-19·github.com
  6. [6]
    antvis/G2 on GitHub
    reporting·github.com
  7. [7]
    Mini Shai-Hulud is back: TanStack compromised (Aikido precursor)
    reporting·2026-05-12·aikido.dev

Related incidents

2026-05-12·CRITICAL
Mini Shai-Hulud npm Worm: TanStack, UiPath, Mistral AI and 169 Packages Compromised (May 2026)
2021-11-04·CRITICAL
rc and coa Coordinated npm Account Takeover (2021)
2021-10-22·CRITICAL
ua-parser-js npm Account Compromise (2021)
2026-04-22·CRITICAL
Bitwarden CLI Supply Chain Compromise (2026)
2026-05-04·HIGH
microsop npm Cluster: Dependency-Confusion Campaign Targeting Apple Internal CI/CD (2026)
last reviewed / 2026-05-19reviewer / benlicense / CC BY 4.0
// incident metadata
severity
CRITICAL9.4
status
confirmed
disclosed
2026-05-19
occurred
2026-05-19 → 2026-05-19
vector
npm supply chain
platforms
npmGitHubAWSGCPAzure
tokens
GitHub PATGitHub App TokenGitHub OAuth Tokennpm Publish TokenAWS Access KeyAWS Session TokenGCP Service AccountAzure Service PrincipalSSH Private KeyAPI Key (generic)Database CredentialEnvironment Variable
nhi severity index
score9.4 / 10blast radiusecosystem-widereachabilityproductionprivilegevariable
nhi kill chain
Ghost Key↗Drifted Key↗Unattributed Key↗
metrics
exposure1 days