Shai‑Hulud 2.0: an IR playbook for the npm preinstall worm

Hands‑on guidance to hunt, contain, and evict the ongoing Shai‑Hulud 2.0 npm supply‑chain campaign using preinstall hooks, Bun‑based load...

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:
    • setup_bun.js and bun_environment.js in package tarballs or node_modules; auxiliary files cloud.json, contents.json, environment.json, truffleSecrets.json. (wiz.io)
    • Suspicious GitHub workflows: .github/workflows/discussion.yaml and formatter_*.yml. (wiz.io)
  • 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

  1. 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)
  1. Rotate credentials broadly
  • Treat GitHub PATs, npm tokens, and cloud credentials (AWS/GCP/Azure) as exposed; rotate and scope/restrict new tokens. (wiz.io)
  1. 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)
  1. 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)
  1. 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)
  1. 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