You know that feeling when you npm install
and suddenly your project has 1,000+ dependencies even though you didn't add them or your package.json doesn't have them? Yeah, that's not just a performance concern anymore. A sophisticated supply chain attack called "Shai-Hulud" has been wreaking havoc in the npm ecosystem, and it's a wake-up call for all of us to be more careful with our dependencies.
This isn't just another security scare - it's a real threat that's already compromised hundreds of packages and could be sitting in your node_modules
right now. Let me walk you through what's happening and, more importantly, how to protect your projects now and in the future.
Think of this attack as a digital virus that spreads through the npm ecosystem. It's clever, automated, and uses legitimate tools against us. Here's the playbook:
It all starts with a classic move - phishing emails that look like legitimate npm security alerts. Maintainers of popular packages get tricked into giving up their credentials. You know those emails that make your heart skip a beat? Yeah, those.
Once they have access, attackers inject malicious code into the packages. The attack started with @ctrl/tinycolor
(a popular color utility), but it's spread like wildfire since then.
One of the most alarming aspects of this attack is its worm-like behavior. Instead of infecting just a single package, the malware is engineered to automatically re-publish itself into every npm package owned by the compromised maintainer. Here’s how it works: it downloads a target package tarball from the npm registry, bumps the patch version in package.json
, and adds a malicious lifecycle hook (like postinstall
). The worm copies its own payload into the package (often as bundle.js
), then re-publishes the trojanized package back to npm using the maintainer’s credentials. This cycle repeats for every package the maintainer controls, turning each one into a new infection vector. As soon as someone installs any of these packages, the worm executes and continues to propagate—no manual intervention required. Once a single account is compromised, the malware leverages the maintainer’s publishing rights to rapidly and automatically spread across the ecosystem.
The malware uses TruffleHog (ironically, a legitimate security tool) to:
webhook.site
)If it finds GitHub tokens, it goes nuclear:
The workflow names are usually something like shai-hulud-workflow.yml
- pretty on the nose, right?
Remember when you thought "it's just a small utility package, what could go wrong?" Well, this is what could go wrong. Here's the real impact:
Your CI/CD Pipeline Could Be Compromised
Your Source Code Could Be Stolen
Your Infrastructure Could Be Hijacked
The Domino Effect
One compromised package can infect hundreds of others. Your package.json
might look innocent, but transitive dependencies could be carrying malware. You can find these in your package-lock.json
or yarn.lock
file.
Don't panic, but let's check if you're already compromised. Here's your 5-minute security audit:
Check if you have any compromised packages like @ctrl/tinycolor
(4.1.1, 4.1.2).
Refer to the "Impacted packages" table at the end of this blog post to see the full list of affected packages and versions. If you find any of these in your dependencies, take immediate action to remove or update them!
# Check your GitHub Actions find .github/workflows -name "*.yml" -exec grep -l "shai-hulud\|webhook.site" {} \; # Look for suspicious workflow names ls -la .github/workflows/ | grep -v "main\|test\|deploy"
Check your logs for connections to:
webhook.site
ngrok.io
# On macOS/Linux, check recent network connections netstat -an | grep -E "(webhook\.site|ngrok\.io)"
Alright, enough doom and gloom. Here's are some of the things you can do to protect yourself:
Try to avoid installing everything under the sun. If you don't need a package or can write it yourself in say 20 lines, then don't install it. Try to get an idea of what package you are installing and if it is a popular package, then check if it is well maintained.
We usually enable auto-upgrade patch versions like below in package.json
. But it is better to avoid this and use the exact version. Otherwise if there is a vulneable patch, your app will also start using that.
{ "dependencies": { "lodash": "~4.17.21", // Will automatically upgrade to 4.17.22, 4.17.23 etc "react": "~18.2.0" // Will automatically upgrade to 18.2.1, 18.2.2 etc } }
Implement static security checks like npm audit
, Snyk
etc to check for vulnerabilities in your dependencies in your CI/CD pipeline.
Implement automated and manual security checks for your application if possible before deploying to production.
Before installing anything new:
# Check package health npm view package-name npm view package-name time npm view package-name maintainers # Look for red flags: # - Last updated > 2 years ago # - Few downloads # - No recent commits
Train your team on dependency security and how to identify malicious packages.
Make security everyone's job:
The open-source ecosystem is amazing, but it's built on trust. That trust is being exploited. We can't stop using packages, but we can be smarter about how we use them.
Remember: every dependency is a potential attack vector. Treat them like you would treat any other security risk - with respect, caution, and proper controls.
Stay safe out there, and happy coding! 🚀
Here's the complete list of packages known to be compromised in the 'Shai-Hulud' attack. Check your package-lock.json
and node_modules
immediately if you're using any of these change the version to the latest one or one before the compromised version.
Package | Versions |
---|---|
@ahmedhfarag/ngx-perfect-scrollbar | 20.0.20 |
@ahmedhfarag/ngx-virtual-scroller | 4.0.4 |
@art-ws/common | 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/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 |
@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 |
@nativescript-community/ui-material-core-tabs | 7.2.72, 7.2.73, 7.2.74, 7.2.75 |
@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 |
@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 |
@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 |
@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 |
@teselagen/bounce-loader | 0.3.16, 0.3.17 |
@teselagen/liquibase-tools | 0.4.1 |
@teselagen/range-utils | 0.3.14, 0.3.15 |
@teselagen/react-list | 0.8.19, 0.8.20 |
@teselagen/react-table | 6.10.19 |
@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 |
globalize-rpk | 1.7.4 |
graphql-sequelize-teselagen | 5.3.8 |
html-to-base64-image | 1.0.2 |
json-rules-engine-simplified | 0.2.1 |
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 |
ove-auto-annotate | 0.0.9 |
pm2-gelf-json | 1.0.4, 1.0.5 |
printjs-rpk | 1.6.1 |
react-complaint-image | 0.0.32 |
react-jsonschema-form-conditionals | 0.3.18 |
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 |
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 |
ve-editor | 1.0.1 |
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 |
To stay updated with the latest updates in Software Engineering, follow us on LinkedIn and Medium.
Complete guide to HTTP protocol evolution. Compare HTTP/0.9, HTTP/1.0, HTTP/1.1, HTTP/2, and HTTP/3 with improvements and use cases.
Complete comparison of proxy servers, reverse proxies, and load balancers. Learn key differences, use cases, configuration examples, and when to use each technology in modern web architectures.
Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.