How to Recover echarts-for-react After the May 2026 AntV Attack
Writing
SECURITY
Published August 5, 202611 min read

How to Recover echarts-for-react After the May 2026 AntV Attack

Step-by-step incident response after the May 19 @antv npm worm hit echarts-for-react. Audit lockfile, pin safe versions, rotate tokens, scan history.

Rabinarayan Patra - Software Development Engineer

By Rabinarayan Patra

SDE II at Amazon

echarts-for-reactantv-attackmini-shai-huludsupply-chain-securitynpmincident-response

I got the Slack notification on the morning of May 20 from a dashboard project I do not even maintain anymore. The CI had failed overnight with a weird npm install error. By the time I logged in, the actual story had broken on Hacker News. A compromised npm maintainer named atool had published 639 malicious versions across 323 packages in a 22-minute burst on May 19, 2026. The hit list included echarts-for-react, with 1.1 million weekly downloads, and every major package under @antv.

If your team ran npm install between roughly 01:39 and 02:06 UTC on May 19, your CI runner or developer machine probably executed the payload. The malware was a preinstall script that harvested over 20 credential types and shipped them to attacker-controlled GitHub repos before the install even finished resolving.

This post is not a postmortem. It is a recovery playbook. If you shipped anything during the bad window, work through these steps in order. The first three are about containment. The last three are about hardening so the next wave does not land.

What happened in the May 2026 @antv npm attack?

On May 19, 2026, an attacker (tracked as TeamPCP by Socket, attributed by Microsoft as part of the Mini Shai-Hulud campaign) compromised the npm maintainer account atool. Over a 22-minute window, the attacker published 639 malicious versions spanning 323 packages, including the full @antv data-visualization suite, echarts-for-react, timeago.js, size-sensor, and canvas-nest.js.

The payload was a preinstall script with the entry "bun run index.js". On install, it ran a credential harvester that scraped at least 20 credential types: AWS access keys, GCP service accounts, Azure secrets, GitHub Personal Access Tokens, npm tokens, SSH private keys, kubeconfig files, and Vault tokens. It then created public GitHub repositories on stolen accounts (with names like sayyadina-stillsuit-852) and committed the harvested credentials there as a fallback exfiltration channel in case the primary C2 at t[.]m-kosche[.]com was blocked.

GitHub eventually invalidated 61,274 npm tokens with write access and 2FA bypass. The malicious package versions were unpublished. But anything those tokens already touched is suspect, and anything those exfiltrated credentials can still log into is fair game for an attacker. Cleanup is on you.

The earlier TanStack attack on May 11 used a different primitive (a GitHub Actions cache-poisoning bug that produced valid SLSA Build Level 3 attestations). This attack reverted to the classic playbook: own a maintainer account, push malicious versions, wait for npm install. Different mechanism, same outcome.

How do you know if your app pulled a poisoned echarts-for-react version?

You know if your package-lock.json or pnpm-lock.yaml or yarn.lock resolved echarts-for-react or any @antv/* package to a version published between May 19, 2026 01:39 UTC and 02:06 UTC. There is no ambiguity. Any install in that window pulled a poisoned tarball.

Run this from your project root:

# npm
jq '.packages | to_entries[] | select(.key | test("echarts-for-react|@antv/|size-sensor|timeago\\.js|canvas-nest\\.js")) | {key, version: .value.version, resolved: .value.resolved}' package-lock.json
 
# pnpm
grep -E "echarts-for-react|@antv/|size-sensor|timeago\.js|canvas-nest\.js" pnpm-lock.yaml | head -50
 
# yarn (v1)
grep -E "^(echarts-for-react|@antv/|size-sensor|timeago\.js|canvas-nest\.js)" yarn.lock -A 2

Then check each resolved version against the npm registry publish timestamp:

npm view echarts-for-react@<version-from-lockfile> time --json

If the timestamp falls in the bad window, you are hit. If not, you are clean for that package. Repeat for every match in the lockfile.

If the lockfile lookup is too slow because you have dozens of @antv/* deps, run a faster fleet check. Grep your CI runs for preinstall activity on May 19:

# Search CI logs for the preinstall payload signature
grep -r "bun run index.js" .github/ ci-logs/ 2>/dev/null
 
# Or query GitHub Actions runs (gh CLI)
gh run list --workflow=ci.yml --created '2026-05-19' --json conclusion,databaseId,headBranch

Any green run from that day that ran npm ci against a lockfile resolving to bad versions exfiltrated credentials. Treat the runner as compromised.

Which echarts-for-react and @antv versions are safe to pin?

Any version published before May 19, 2026 01:39 UTC is safe. The compromised window is bounded. Versions published after the unpublish event on May 20 are also safe, because the maintainer account has been recovered and the malicious versions are gone from the registry.

The fastest way to find the right pin is to list all versions with timestamps:

npm view echarts-for-react versions --json | jq -r '.[]' | tail -20
npm view echarts-for-react time --json | jq 'to_entries | sort_by(.value) | .[-20:]'

Pin to the last entry whose time value is before 2026-05-19T01:39:00Z. Same for every @antv/* package in your lockfile.

To enforce the pin across the workspace, use overrides. For npm:

{
  "overrides": {
    "echarts-for-react": "3.0.4",
    "@antv/g2": "5.2.20",
    "@antv/x6": "2.18.1",
    "@antv/graphin": "3.0.4"
  }
}

For pnpm:

# pnpm-workspace.yaml or package.json
pnpm:
  overrides:
    echarts-for-react: 3.0.4
    "@antv/g2": 5.2.20

The version numbers above are placeholders. Look up the actual safe versions yourself with npm view, because new safe versions ship continuously and a static post will go stale. Overrides force the resolver to ignore the transitive dependency graph and pin everywhere, including inside nested deps.

How do you clean the lockfile and reinstall safely?

Delete node_modules and the lockfile, then reinstall with --ignore-scripts so any residual malicious preinstall hook in your tree does not run. This is the single most important step in the whole playbook. Skipping --ignore-scripts on a poisoned lockfile is what trips most teams trying to clean up.

# Stop the dev server, kill any watchers
rm -rf node_modules
rm -f package-lock.json
# (or pnpm-lock.yaml / yarn.lock)
 
npm install --ignore-scripts

If you also have leftover untracked artefacts in your working tree from the poisoned install (random dot-files, generated test fixtures), use git clean with the -n dry run first so you do not nuke a stash or unstaged work.

Now audit the regenerated lockfile against the same grep from earlier. The versions should all be either pre-attack or post-recovery. If anything still resolves to a bad version, your overrides did not bite and you need to track down which transitive dep is pulling it in:

npm ls echarts-for-react
npm ls @antv/g2

npm ls shows the dependency chain. Whatever package brought in the bad version needs to be updated or temporarily replaced.

Once the lockfile is clean, run the install one more time without --ignore-scripts so any legitimate postinstall hooks (like patches or native compiles) run as expected:

npm install
npm run build
npm test

If the test suite passes and the build is green, your application code is back. Now you can deal with the credentials.

How do you rotate GitHub Actions secrets and npm tokens?

Rotate everything the payload could have seen. The payload ran inside npm install, which means it had access to whatever the runner had access to. On a typical GitHub Actions runner, that includes the entire GITHUB_TOKEN, every repository secret, every organization secret pulled into the job, the npm token in .npmrc if present, and any cloud credentials injected as env vars.

Walk this list in order:

  1. npm tokens. Visit npmjs.com settings, Access tokens. Revoke every automation token used in the affected workflows. Re-create new ones, or better, migrate to npm trusted publishing with OIDC so you do not need long-lived tokens at all.
  2. GitHub repository secrets. In each affected repo, Settings then Secrets and variables then Actions. Rotate every secret used in workflows that ran on May 19 or that share runners with affected workflows. Treat shared organization secrets the same way.
  3. GitHub Personal Access Tokens. Go to your account Settings then Developer settings then Personal access tokens. Revoke every classic PAT and fine-grained PAT older than May 19. Re-issue with minimum required scopes.
  4. Cloud credentials. AWS access keys, GCP service-account keys, Azure client secrets used by the affected workflows must all be rotated. Use the cloud console to identify keys last used by the affected runner.
  5. SSH keys deployed to runners. If your runners had SSH keys to deploy to staging or production, rotate them. The payload reads ~/.ssh/ directly.
  6. Vault tokens, Kubernetes configs, Datadog API keys, anything else you mounted into the job env. All of these were enumerated and exfiltrated. Rotate every one.

After rotation, audit the actual usage. AWS CloudTrail, GCP Audit Logs, and GitHub's audit log can show whether any of the rotated keys made an unusual API call between May 19 and the moment you revoked them. Anything unusual gets escalated to a full incident.

How do you scan git history for leaked credentials?

Run a fresh credential scan against the affected repository because the attacker may have pushed commits with exfiltrated data back into your repo using your own write tokens, or the rotation step might surface previously committed secrets that you never noticed. Both paths happen in real incidents.

Two tools that work well for this:

# trufflehog: live scan against the full git history
docker run --rm -v "$PWD:/repo" trufflesecurity/trufflehog \
  git file:///repo --only-verified
 
# gitleaks: faster, config-driven, can run in CI
docker run --rm -v "$PWD:/repo" zricethezav/gitleaks \
  detect --source /repo --verbose --no-banner

--only-verified on TruffleHog means it actually tests each finding against the relevant API to confirm it is a live credential, not just a string that looks like one. That cuts noise hard.

For each verified finding:

  1. Revoke the credential at the source (provider console).
  2. Remove the secret from history with git filter-repo (do this in a separate branch first, then force-push only after team sign-off, and never on a shared branch without explicit coordination). If a teammate accidentally deletes the branch holding their cleanup work, the git reflog recovery pattern gets it back.
  3. Add the leaked pattern to your gitleaks.toml baseline so it does not flag on every CI run.

If the leaked credential was a database password, rotate that too. The payload could have read environment variables on developer laptops that pulled the .env file from disk.

How do you harden CI so the next wave does not land?

Three controls would have stopped the AntV payload cold, and adopting them now means the next wave does not become your next weekend of work.

Run npm install with --ignore-scripts everywhere in CI. Lifecycle scripts (preinstall, install, postinstall) are the universal attack vector for npm worms. The Microsoft postmortem explicitly recommends this. Add ignore-scripts=true to your .npmrc for CI runners and let only known-trusted packages run their scripts via an allowlist. Tools like @lavamoat/allow-scripts make this practical.

Pin lockfile versions and verify with a fast registry alternative. The faster your detection, the less time you spend in the bad window. Configure Renovate or Dependabot to alert on any dependency update, and use Socket's GitHub App or Snyk to flag malicious-package indicators before merge. The AntV payload was flagged by Socket within 6.7 minutes of publication. That is faster than most teams check Slack.

Switch publishers off long-lived npm tokens. This is what closes the underlying class. The atool account fell because the credential was steal-able and reusable. Migrate publishing workflows to OIDC-based trusted publishing so even a fully compromised maintainer account cannot republish from an attacker's machine. The full setup is in my npm trusted publishing guide.

For an extra layer, gate every publish workflow on a GitHub Environment with required reviewers. The OIDC token is only minted after a human approves the deployment. Combined with tag-only triggers (no publish on every main push), this means the attacker needs the source repo AND a human reviewer AND a tag push, not just a credential.

What should you do this week?

If you shipped anything that touches the npm ecosystem, do these in this order today:

  1. Run the lockfile audit grep. Identify whether you are in the bad window.
  2. If yes, treat affected runner credentials as burned and rotate them.
  3. Regenerate the lockfile with --ignore-scripts, pin safe versions via overrides.
  4. Run trufflehog or gitleaks against the full git history.
  5. Set ignore-scripts=true in .npmrc for CI.
  6. Schedule a follow-up to migrate publishing to OIDC trusted publishing.

The Mini Shai-Hulud campaign is not over. The TanStack attack on May 11 and the AntV attack on May 19 were eight days apart, and the same actor or copycats will hit another popular maintainer account before the year ends. The defenses above stop the class of attack, not just this incident.

For more on this, see Microsoft's Mini Shai-Hulud writeup, Socket's detection postmortem, and StepSecurity's incident analysis.

Keep Reading

Frequently Asked Questions

What is the May 2026 @antv npm attack?

On May 19, 2026, a compromised npm maintainer account named atool published 639 malicious versions across 323 packages in roughly a 22-minute window. The hit list included echarts-for-react and the entire @antv data-visualization ecosystem. The payload ran on npm install as a preinstall script and harvested over 20 credential types from the developer or CI machine.

How do I know if my echarts-for-react install is compromised?

Check the resolved version in your lockfile against the malicious versions published between 01:39 and 02:06 UTC on May 19, 2026. Any echarts-for-react, @antv/*, size-sensor, timeago.js, or canvas-nest.js install that happened between May 19 and May 20 should be treated as suspect until you have audited the lockfile and the package.json prepare hooks.

Which versions of echarts-for-react are safe?

Any version published before May 19, 2026 01:39 UTC is safe. The compromised account published only malicious versions during the burst window. Run npm view echarts-for-react versions --json to see the full list with timestamps and pin to the last release published before that window.

Will rotating GitHub tokens fix the breach?

Rotating tokens is necessary but not sufficient. The payload exfiltrated GitHub PATs, npm tokens, AWS keys, GCP keys, Azure keys, SSH keys, Kubernetes configs, and Vault credentials, so every credential type the runner could see is potentially burned. You also need to revoke OAuth app authorizations, rotate webhook secrets, and search git history for any committed credentials.

Rabinarayan Patra - Software Development Engineer

Rabinarayan Patra

SDE II at Amazon. Previously at ThoughtClan Technologies building systems that processed 700M+ daily transactions. I write about Java, Spring Boot, microservices, and the things I figure out along the way. More about me →

X (Twitter)LinkedIn

Stay in the loop

Get the latest articles on system design, frontend and backend development, and emerging tech trends, straight to your inbox. No spam.