Auditing npm Dependencies: Detecting Malicious Packages Before They Run

Malicious npm packages continue to target developer environments through typosquatting, dependency confusion, compromised maintainer accounts, and postinstall script abuse. This guide covers practical tools and techniques to catch malicious packages before they execute in your pipeline.

In June 2026, 144 packages in the Mastra namespace were compromised by a single attacker mass-publishing malicious variants. In October 2025, a batch of malicious JetBrains Marketplace plugins with 70,000 installs were found exfiltrating API keys. These aren’t unusual events: the npm registry logs tens of thousands of package removal requests per year for malicious or suspicious packages. The volume of malicious package attempts continues to increase because the payoff is high: a successful postinstall script on a developer machine, or in a CI/CD pipeline, can exfiltrate credentials, modify build output, or persist backdoors.

The good news is that practical defences exist and don’t require significant tooling investment.

Understanding the Attack Patterns

Before choosing defences, it’s worth being clear about what you’re defending against:

Typosquatting: Publishing a package with a name visually similar to a popular one. lodash vs 1odash, axios vs axois. These rely on developers not noticing the misspelling or IDEs autocompleting to the wrong name.

Dependency confusion: Publishing a public package with the same name as a private internal package. npm’s resolution previously preferred the higher version number from any registry — attackers publish public packages at version 9999.0.0 to override internal packages fetched from private registries.

Compromised maintainer accounts: Legitimate packages acquired or taken over by attackers through credential theft or npm account abandonment. The ua-parser-js and node-ipc incidents demonstrated this pattern at scale.

Postinstall script abuse: npm packages can run arbitrary scripts during npm install via postinstall in package.json. This is used legitimately (native module compilation), but malicious packages use it to immediately run code at install time, before the package is ever imported.

Namespace squatting: Registering common variations of package namespaces before the legitimate owner does, then publishing confusing packages under them.

Tool 1: npm audit

npm audit is the baseline. It checks your installed dependencies against the npm advisory database for known vulnerabilities:

npm audit                      # Full audit report
npm audit --audit-level=high   # Exit non-zero only for high/critical
npm audit --json               # Machine-readable output for CI
npm audit fix                  # Auto-fix where possible
npm audit fix --force          # Fix including breaking changes (review carefully)

npm audit tells you about known vulnerabilities in published packages. It does not detect novel malicious packages or those that haven’t been flagged yet. Use it as one layer, not the whole defence.

In CI/CD:

# GitHub Actions step
- name: Run npm audit
  run: npm audit --audit-level=high
  # Fails the pipeline if high or critical vulns are found

Tool 2: Socket.dev — Detecting Malicious Behaviour

Socket.dev analyses package behaviour rather than just known CVEs. It flags packages that:

  • Install scripts that make outbound network requests
  • Read environment variables (potential credential exfiltration)
  • Write to filesystem locations outside their package directory
  • Have unusual binary executables included
  • Show rapid ownership changes (potential account takeover)
# Install Socket CLI
npm install -g @socket/cli

# Analyse a package before installing it
socket scan npm install PACKAGE_NAME

# Run a full scan on current project
socket npm audit

Socket’s free tier covers open-source projects; paid plans support private registries. Their CI/CD integration provides a pull request check that blocks packages with malicious indicators before they enter the codebase.

Tool 3: Lockfiles and npm ci

The npm lockfile (package-lock.json) pins exact versions AND records the integrity hash (SHA-512) for every installed package. npm ci uses the lockfile exclusively, never upgrading anything, and verifies the integrity hash of every package against what was recorded when the lockfile was generated.

# Always use npm ci in CI/CD pipelines — not npm install
npm ci

# Generate or update lockfile
npm install   # Updates package-lock.json with current resolutions

Critical: commit package-lock.json to source control. A missing or gitignored lockfile means your CI/CD pipeline can silently install different package versions than developers tested locally.

If a lockfile entry’s integrity hash doesn’t match the downloaded package, npm ci will fail the installation:

npm error `sha512-HASH` integrity checksum failed when using sha512: 
wanted sha512-EXPECTED but got sha512-ACTUAL

This catches both corrupted downloads and cases where a package version has been tampered with post-publication (rare but has occurred).

Tool 4: Verifying Signatures with npm audit signatures

npm introduced package provenance and signing in 2023. For packages published with the --provenance flag, you can verify the package was built from the claimed source repository:

npm audit signatures

This checks the signature chain for all installed packages. Output shows which packages have verifiable signatures and which don’t. Packages lacking signatures aren’t necessarily malicious, but packages with signature verification failures warrant investigation.

Blocking Postinstall Scripts

The highest-risk npm attack vector is the postinstall script. If you cannot verify that a package’s install scripts are safe, blocking them prevents code execution at install time:

# Install without running any scripts
npm install --ignore-scripts

# Or set permanently in .npmrc
echo "ignore-scripts=true" >> .npmrc

The tradeoff: some legitimate packages (native modules like bcrypt, sharp, better-sqlite3) require postinstall compilation to function. With ignore-scripts, you’ll need to explicitly compile them:

# Run build scripts for specific packages after --ignore-scripts install
npm rebuild bcrypt
npm rebuild better-sqlite3

For most web application projects with no native module dependencies, --ignore-scripts adds no friction and eliminates the entire postinstall code execution surface.

Detecting Dependency Confusion

Configure npm to use your private registry for internal package namespaces and prevent fallback to the public registry:

# .npmrc — scope your internal packages to your registry
@your-org:registry=https://your-registry.example.com/
//your-registry.example.com/:_authToken=${NPM_TOKEN}

# Explicitly block public resolution for internal scopes
@your-org:registry=https://your-registry.example.com/
@your-org:always-auth=true

With this configuration, any package in the @your-org scope will only resolve from your private registry and never fall back to npmjs.com — even if an attacker publishes a higher-versioned package there.

For packages without an @org scope (legacy internal package names), the protection is harder. Prefer migrating them to a scoped namespace.

Generating an SBOM

A Software Bill of Materials lets you track what dependencies you’re actually shipping and check them against vulnerability databases:

# Generate SPDX SBOM from npm
npm sbom --sbom-format=spdx --sbom-type=package > sbom.spdx.json

# Generate CycloneDX format
npx @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json

Run SBOM generation in your release pipeline and archive the output. If a new vulnerability is disclosed, you can query historical SBOMs to determine which of your deployed versions are affected without re-running the build.

Checklist for New Dependencies

Before adding a new npm package to a project, a 90-second check covers most obvious risks:

  1. Check npm download count: Is it genuinely used? Packages with 50 weekly downloads posing as popular utilities are suspicious
  2. Check publication date and author history: A package registered two days ago by a brand-new account is higher risk
  3. Review package.json scripts: Does it have preinstall, install, or postinstall scripts? What do they do?
  4. Check the repository URL: Does it link to a real, active repository? Does the code match what’s in the registry?
  5. Run Socket scan: socket scan npm install PACKAGE_NAME before installing
# Quick pre-install check
npx npm-audit-extra --check-scripts PACKAGE_NAME

# Or use Socket
socket scan npm install PACKAGE_NAME

These steps take less time than reading a package’s README and catch the vast majority of obviously malicious packages. For high-trust pipelines — CI/CD that deploys to production — combining lockfile integrity, npm ci, postinstall blocking, and Socket scanning provides defence-in-depth against all current npm attack patterns short of a compromised legitimate maintainer.