How to Detect Malicious Code in npm Packages Before Installing

··12 min read
How to Detect Malicious Code in npm Packages Before Installing

Every time you run npm install, you're inviting hundreds of strangers into your project. A single dependency like express pulls in dozens of transitive packages, and each of those was published by someone you've never met, running code that executes with the full permissions of your build machine. That's not paranoia. In 2024, security researchers logged over 700,000 malicious packages across open source registries, and npm was the single largest target.

The attacks have gotten clever. Gone are the days of obviously named steal-your-bitcoin packages. Today's threats hide inside typosquatted names one keystroke off a popular library, or slip into a legitimate package after a maintainer's account gets phished. The infamous event-stream incident compromised a package with over 8 million weekly downloads by injecting a payment-stealer into a nested dependency that almost nobody was auditing.

This guide walks through exactly how to detect malicious npm packages before they ever touch your codebase. You'll learn the manual inspection techniques senior engineers use, the automated tooling that scales those checks across a full dependency tree, and a repeatable workflow you can adopt today. No fluff, just the checks that actually catch things.

Key Takeaways
  • Inspect before you install. Use npm view and the npm web page to review a package's age, download trend, maintainers, and repository link before running install.
  • Install scripts are the number one attack vector. Malicious packages abuse preinstall and postinstall hooks; disable them with --ignore-scripts for untrusted code.
  • Automate with layered tooling. Combine npm audit, Socket, and Snyk. No single tool catches everything.
  • Watch for red flags: typosquatted names, brand-new versions, obfuscated or minified source, and unexpected network calls during install.
  • Lock everything down. Commit your lockfile, pin versions, and use npm ci in CI to guarantee reproducible, reviewed installs.

Why npm Packages Are Such an Attractive Target

npm hosts over 3 million packages, and the average front-end project depends on more than 1,000 of them once you count transitive dependencies. That sprawl is the whole problem. You audit the one library you chose, but you rarely read the 40 packages it quietly dragged along.

Attackers exploit three structural weaknesses:

  • Automatic code execution. npm packages can run arbitrary shell commands during installation through lifecycle scripts. No user consent required.
  • Trust by transitivity. You trust package A, so you implicitly trust everything A depends on, and everything those depend on.
  • Publish speed. Anyone can publish a package in under a minute. There's no mandatory review before it goes live to millions of developers.

The same discipline applies to any third-party code you bring into production, whether it's a Node module or a WordPress plugin. If you want the broader framework, our guide on how to vet open source software before using it in production pairs well with everything below.

Manual Inspection: The Checks You Do Before Installing

Before you type npm install some-package, spend two minutes doing reconnaissance. These checks cost almost nothing and catch the majority of low-effort attacks.

1. Read the registry metadata

Run npm view <package> to pull the package's public record without installing it. You get the maintainers, latest version, dependencies, and publish dates. Pay attention to:

  • Package age. A package published three days ago with a name suspiciously close to a popular library is a classic typosquat.
  • Version history. A jump from 1.2.3 to a brand-new 1.2.4 published hours ago, with no corresponding GitHub commit, deserves scrutiny.
  • Maintainer list. If a well-known package suddenly gained a new maintainer nobody recognizes, that can signal an account takeover.

2. Check download trends and the repository link

Open the package's npm page. A legitimate utility with 50,000 weekly downloads but a GitHub repo showing 4 stars and no commits in two years is a mismatch worth investigating. Click through to the repository. If the package.json on npm points to a repo that doesn't exist, or the code on GitHub doesn't match what npm serves, walk away.

3. Look at what runs during install

This is the single most important check. Inspect the scripts block for preinstall, install, and postinstall hooks:

npm view <package> scripts

A pure JavaScript library has no business running a shell script that curls a remote URL during installation. If you see something like postinstall: "node ./scripts/setup.js" and the package is a simple date formatter, download and read that script before proceeding.

4. Download without executing

You can fetch and unpack a package tarball without triggering any scripts:

  1. Run npm pack <package> to download the .tgz without installing.
  2. Extract it with tar -xzf <package>-<version>.tgz.
  3. Read the actual files, especially anything in a scripts/ folder or any minified .js that ships without a source map.

Heavily obfuscated code in a package that claims to be a small utility is a giant red flag. Legitimate maintainers ship readable source or, at minimum, minified builds with matching sources.

A Worked Example: Vetting a Suspicious Package

Let's make this concrete. Say you're about to install a color-parsing helper and you find two candidates: color-string (the real one) and colour-string (a lookalike you found in a Stack Overflow answer). Here's the two-minute audit.

  1. Run npm view colour-string. You see it was published 6 days ago, has 1 maintainer with a random username, and 0 dependents. The real color-string has 4,000+ dependents and years of history.
  2. Check the scripts. npm view colour-string scripts returns postinstall: "node index.js". A color parser has no reason to run itself on install.
  3. Pack and read it. You run npm pack colour-string, extract, and open index.js. Inside you find a base64-encoded string being passed to eval() and a call to https://a-random-domain.io/collect sending your environment variables.
  4. Verdict: confirmed credential stealer. You install the real color-string instead and move on.

That entire process took under five minutes and prevented your CI secrets, cloud tokens, and possibly your production keys from being exfiltrated on the next build. This is the same forensic mindset we apply when we teach readers to audit a WordPress plugin for hidden backdoors: read the code, follow the network calls, distrust obfuscation.

Automated Detection Tools Compared

Manual review doesn't scale to a 1,200-package dependency tree. You need tooling that flags problems automatically. Here's how the main options stack up.

Tool Detects install scripts Behavioral analysis Known CVE scanning Cost Best for
npm audit No No Yes Free (built in) Baseline vulnerability check
Socket Yes Yes Partial Free tier + paid Catching supply-chain behavior
Snyk Partial Limited Yes Free tier + paid CI integration and fix PRs
OSV-Scanner No No Yes Free (Google) Cross-ecosystem CVE data
npm pack + manual Yes Manual No Free Deep review of one package

My honest take after using all of these: npm audit is necessary but insufficient, because it only knows about already-reported vulnerabilities. It will not catch a zero-day malicious package published this morning. Socket is the standout for supply-chain threats specifically because it analyzes what a package actually does: does it read the filesystem, spawn a shell, open a network connection, use eval? That behavioral signal catches novel attacks that signature-based scanners miss.

Running the free baseline

At minimum, add this to your workflow:

  1. Run npm audit after every install and read the output rather than blindly running npm audit fix.
  2. Install the Socket GitHub app on your repositories so every pull request that adds a dependency gets an automated risk report.
  3. Add osv-scanner to CI for a second opinion on known vulnerabilities across your lockfile.

Hardening Your Install Workflow

Detection is only half the job. Configure npm so a malicious package has fewer opportunities to do damage even if one slips through.

Disable install scripts by default

The nuclear option that prevents nearly all install-time attacks is to block lifecycle scripts:

npm install --ignore-scripts

You can make this permanent by adding ignore-scripts=true to your .npmrc. The tradeoff: some legitimate packages (native modules like node-sass or bcrypt) genuinely need their build scripts. So maintain a short allowlist and run scripts only for those, after reviewing them.

Pin versions and commit your lockfile

Loose version ranges like ^1.2.0 let a compromised patch release silently enter

Cover image: ITU Workshop on Zero Trust and Software Supply Chain Security by Bulexat, licensed under BY-SA 4.0 via Openverse.

Recent Posts

View all →

Most Popular Software

View all →

Browse by Platform

View all →