RC RANDOM CHAOS

Shai-Hulud worm compromises 314 npm packages

Shai-Hulud npm worm hits 314 more packages via compromised maintainer accounts. Mechanism, telemetry gaps, and residual exposure analyzed.

· 6 min read

Shai-Hulud is back in the npm registry. 314 packages compromised in the latest wave after another maintainer account takeover. The worm self-propagates by harvesting credentials from infected publish environments and using them to push poisoned versions of every other package the victim controls. Same family as the September 2025 outbreak that hit @ctrl/tinycolor and the CrowdStrike-published packages. Same primitive. Wider blast radius.

The mechanism is well understood at this point. The malicious postinstall script executes the moment a downstream consumer runs npm install. The payload is a bundled TruffleHog binary plus a Node loader. TruffleHog scans the host filesystem and environment for credential strings - AWS access keys, GCP service account JSON, GitHub PATs, npm authTokens, .env files, ~/.aws/credentials, ~/.npmrc. Anything matching a known secret regex is collected. The loader then queries the cloud metadata services. IMDSv1 on AWS at 169.254.169.254. The GCP metadata server with the Metadata-Flavor header. Whatever the host’s instance identity is willing to vend.

Exfiltration goes two places. The first is a public GitHub repository created under the victim’s own account using their harvested PAT. The repo name is Shai-Hulud or a variant. The contents are the harvested secrets in plaintext or base64. The repository is public. This is the signal that gave the campaign its visibility - researchers monitor GitHub for repos named Shai-Hulud and watch the attacker advertise their own intrusions. The second exfil path is a webhook the worm uses for live telemetry to the operator.

The propagation step is what makes this a worm rather than a single-package compromise. Once the payload holds an npm authToken with publish rights, it enumerates the packages the compromised principal can publish. For each package, it pulls the latest version, injects its own postinstall hook into the package.json, increments the patch version, and publishes. The new version inherits the trust of the original. Downstream installs pull the malicious version on next npm install, and the cycle continues. The 314 count is the count of packages observed carrying the payload in this wave. The actual infected principal count is smaller - each compromised maintainer poisons their entire portfolio.

The entry vector for the maintainer accounts themselves is the part that matters and the part that is rarely confirmed. Phishing of npm 2FA recovery flows. Stolen browser session cookies via infostealer logs sold on Russian Market and similar. OAuth token theft from compromised GitHub accounts linked to npm. Reuse of credentials leaked in prior worm iterations. The September 2025 wave was attributed to phishing pages mimicking npmjs.com login. The current wave’s initial access is not publicly confirmed at the time of writing. The pattern that holds across iterations is that npm 2FA - when present at all - was either bypassed via session theft or absent on the compromised accounts.

MITRE mapping is straightforward. T1195.002, supply chain compromise via software dependencies. T1552.001, credentials in files. T1552.005, cloud instance metadata API. T1567, exfiltration over web service, specifically GitHub. T1078.004, cloud accounts, once harvested AWS keys are used downstream. The worm itself is closer to T1080, taint shared content, executed through the package registry as the shared resource.

The attack surface is anything that runs npm install with network egress and ambient credentials. CI/CD runners are the highest-value target. A GitHub Actions runner executing npm ci pulls the malicious transitive dependency, the postinstall fires, and the runner’s GITHUB_TOKEN, OIDC tokens, AWS role credentials assumed via aws-actions/configure-aws-credentials, and any secrets injected into the environment are harvested. Developer workstations are the next tier - .aws/credentials, .ssh/id_rsa, browser-stored npm sessions, GitHub PATs in dotfiles. Production build hosts running unscoped npm tokens are the worst case. The compromise propagates from the build host into the registry into every consumer of every package that host can publish.

What fires in telemetry depends on where the infected install runs. On a CI runner with EDR, the indicators are process-level. Node spawning a child process that resolves to a bundled binary in a temp directory. Outbound HTTPS to api.github.com creating a new repository. Outbound HTTPS to the IMDS endpoint from a Node process - anomalous for most build jobs. Sysmon Event ID 1 captures the process creation. Event ID 3 captures the network connections. Event ID 11 captures the dropped TruffleHog binary on disk. Event ID 22 captures DNS lookups for api.github.com when correlated with a Node parent. The detection logic is the correlation, not any single event.

The gap is that most CI environments do not run EDR. GitHub-hosted runners are ephemeral and lack endpoint telemetry by default. Self-hosted runners often run as root in containers without auditd or any process-level logging. The first observable in those environments is the GitHub repo creation event in the org audit log - and only if the org owns the account being used. If the attacker uses a personal account stolen via PAT, the victim org sees nothing in its own logs. The exfil-to-public-GitHub design exploits this gap deliberately. Defenders cannot see the upload because it goes to an account the defender does not monitor.

Network-side indicators are thin. Outbound HTTPS to api.github.com is normal for almost every build. The only anomaly is the POST to /user/repos creating a new public repository, which requires TLS-terminating inspection or GitHub audit log access to observe. IMDS access from a build host is anomalous for some workloads and routine for others - the signal is workload-dependent. npm registry access is normal by definition. There is no network signature that distinguishes a poisoned npm install from a clean one.

Detection that does work. GitHub Enterprise audit logs filtered for repository.create events with public visibility from service accounts or machine identities that should not be creating repos. Npm publish events on packages outside an expected cadence, especially patch-version bumps published from IPs that do not match the maintainer’s normal pattern. CI runner network policies that block 169.254.169.254 for jobs that have no legitimate reason to query IMDS. Secret scanning on the public GitHub firehose for the literal repository name Shai-Hulud and known indicator strings - this is how researchers track the campaign in near-real-time.

Residual exposure after the 314 packages are unpublished is significant. Lockfiles already resolved to malicious versions will continue pulling them from package mirrors and internal proxies that cached the bad version. Credentials harvested in the current wave are already in the attacker’s possession and will be replayed against AWS, GitHub, and downstream registries for as long as they remain valid. Rotation is not optional. Every secret that touched an environment running npm install against an affected package between the compromise window and unpublish window must be considered burned.

The structural reality is that npm’s trust model has not changed. A single maintainer credential with publish rights to N packages remains a single point of failure for N packages and their entire dependency tree. Mandatory 2FA on npm helps. Trusted publishing via OIDC removes the long-lived authToken from the equation and is the meaningful control - packages published via GitHub Actions OIDC cannot be republished by an attacker holding a stolen authToken, because the token does not exist. Adoption is partial. Until it is universal across high-fanout packages, the next Shai-Hulud wave is a question of when, not if.

The worm did not exploit a CVE. It exploited the registry’s trust model and the operational reality that maintainers reuse credentials, store tokens in plaintext, and run npm install on machines that hold production secrets. The mechanism is boring. The blast radius is not.

See also: NordVPN for tunneled traffic when operating outside controlled networks.


#ad Contains an affiliate link.

Share

Keep Reading

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.