Shai‑Hulud 2.0: an IR playbook for the npm preinstall worm
CERT-FR issued a bulletin on November 27, 2025 describing an active supply-chain campaign against npm packages that began on November 23, 2025, has affected 700+ packages, executes via a preinstall script to harvest secrets, self-propagate, and in some cases destroy user data; they advise hunting for bun_environment.js, auditing installed versions, temporarily freezing updates, and rotating exposed secrets. (cert.ssi.gouv.fr)
Intrusion Flow
- Initial execution (developer laptop or CI): Compromised npm package versions add a package.json preinstall that runs setup_bun.js, which installs or locates the Bun runtime and launches a large payload bun_environment.js. (wiz.io)
- Credential theft and exfiltration: The payload enumerates environment variables and config files and invokes TruffleHog to capture credentials, then exfiltrates to attacker-created GitHub repositories often labeled with Shai-Hulud-themed descriptions; cross-victim exfiltration has been observed. (cert.ssi.gouv.fr)
- Persistence and remote control: The malware registers a self-hosted runner named “SHA1HULUD” and drops a .github/workflows/discussion.yaml workflow so that creating a GitHub Discussion executes arbitrary commands on the victim’s self-hosted runner; a formatter_*.yml workflow is used to collect GitHub Actions secrets into actionsSecrets.json. (wiz.io)
- Self-replication: Stolen npm tokens and GitHub access allow the actor to publish trojanized versions of additional packages under compromised maintainers, driving rapid worm-like spread across ecosystems. (wiz.io)
- Destructive fallback: Analyses report a “dead man’s switch” that may delete user data (e.g., the home directory) when the malware cannot exfiltrate or persist; CERT-FR also warns about possible user-data deletion. (about.gitlab.com)
Key Artifacts to Pull
- Project and dependency state:
- package.json, package-lock.json, yarn.lock, pnpm-lock.yaml (to diff against clean versions announced by vendors such as Postman and PostHog). (blog.postman.com)
- On-disk indicators:
- GitHub infrastructure state:
- Presence of self-hosted runners named “SHA1HULUD.” Use the GitHub REST API to list runners at org/repo scope during triage. (wiz.io)
- Package manager caches and global install context:
- npm cache directory (default on POSIX: ~/.npm with _cacache) for corroborating when/what versions were fetched. (docs.npmjs.com)
Example triage commands (read-only where possible):
# Hunt for loader/payload names across workspaces and agents
rg -n --hidden -g '!**/.git/**' 'bun_environment\.js|setup_bun\.js'
# Identify packages with preinstall hooks
jq -r '.scripts.preinstall? // empty' **/package.json | nl
# Spot suspicious workflows
rg -n --glob '**/.github/workflows/*.yml' 'discussion\.yaml|toJSON\(secrets\)'
# List self-hosted runners at repo scope (needs admin repo token)
curl -s -H 'Accept: application/vnd.github+json' \
-H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/repos/OWNER/REPO/actions/runners | jq '.runners[].name'
Detection Notes
- Preinstall abuse: npm lifecycle scripts execute preinstall before installation completes; focusing on preinstall hooks catches this variant earlier than prior postinstall-based waves. (docs.npmjs.com)
- Ignore scripts in CI during investigation: Set NPM_CONFIG_IGNORE_SCRIPTS=true or pass –ignore-scripts to avoid running lifecycle hooks while you audit. (docs.npmjs.com)
- Lock deterministic installs: Prefer npm ci from a committed lockfile in CI; several vendor write-ups recommend pinning/rolling back to known-clean versions for containment. (koi.ai)
- Hunt for GitHub persistence:
- Query for newly created repos or descriptions containing Shai-Hulud phrases and remove runner registrations; use the REST endpoints for self-hosted runners to enumerate and delete unknown hosts. (wiz.io)
- Environment clues: Presence of Bun runtime installation attempts (e.g., bun install scripts) on hosts that normally don’t use Bun can be a strong signal in this campaign. (wiz.io)
Response Guidance
- Freeze, verify, and eradicate
- Temporarily freeze npm updates and compare installed versions against vendor disclosures; remove compromised versions. (cert.ssi.gouv.fr)
- Clear local state and reinstall cleanly with scripts disabled while investigating: npm cache clean –force; remove node_modules; reinstall pinned versions with –ignore-scripts. (docs.npmjs.com)
- Rotate credentials broadly
- Treat GitHub PATs, npm tokens, and cloud credentials (AWS/GCP/Azure) as exposed; rotate and scope/restrict new tokens. (wiz.io)
- Evict GitHub persistence
- Remove unexpected self-hosted runners and delete malicious workflows (discussion.yaml, formatter_*.yml) across repos/orgs using the GitHub Actions runners APIs and repository workflow reviews. (wiz.io)
- Organization-wide hunting
- Search code, CI artifacts, and endpoints for bun_environment.js and setup_bun.js; reboot endpoints that might be running attacker-installed runners as CERT-FR suggests. (cert.ssi.gouv.fr)
- Vendor specifics to check
- Postman and PostHog have published lists of affected versions; ensure you are not running the listed bad versions and have upgraded to their clean republished releases. (blog.postman.com)
- Hardening going forward
- Enforce script controls in CI (e.g., –ignore-scripts by default, or allow-list specific packages) and require lockfile-only installs; these controls directly reduce exposure to lifecycle-hook malware. (docs.npmjs.com)
Takeaways
- Hunt immediately for bun_environment.js, setup_bun.js, discussion.yaml, and “SHA1HULUD” runners; assume secrets are exposed and rotate. (cert.ssi.gouv.fr)
- Pin and roll back dependencies to known-clean versions; reinstall with scripts disabled while you validate. (wiz.io)
- Audit and remove malicious GitHub workflows and self-hosted runners across repos and orgs. (wiz.io)
- Monitor for re-infection and cross-victim data leakage; the campaign is ongoing and has shown large-scale automated replication. (wiz.io)
Sources / References
- CERT‑FR bulletin (Nov 27, 2025): https://www.cert.ssi.gouv.fr/actualite/CERTFR-2025-ACT-051/
- Wiz Threat Research: Shai‑Hulud 2.0: https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack
- GitLab VR blog on npm attack: https://about.gitlab.com/blog/gitlab-discovers-widespread-npm-supply-chain-attack/
- Koi Security live updates: https://www.koi.ai/incident/live-updates-sha1-hulud-the-second-coming-hundred-npm-packages-compromised
- Aikido write‑up: https://www.aikido.dev/blog/shai-hulud-strikes-again-hitting-zapier-ensdomains
- Postman engineering incident note: https://blog.postman.com/engineering/shai-hulud-2-0-npm-supply-chain-attack/
- PostHog status incident: https://status.posthog.com/incidents/kv3nj636f59c
- npm config: ignore-scripts (v11 docs): https://docs.npmjs.com/cli/v11/using-npm/config/
- npm lifecycle scripts (docs): https://docs.npmjs.com/cli/v8/using-npm/scripts/
- npm cache (v11 docs): https://docs.npmjs.com/cli/v11/commands/npm-cache/
- GitHub REST API: self‑hosted runners: https://docs.github.com/rest/actions/self-hosted-runners
- Bun installation docs: https://bun.sh/docs/installation