Key takeaways
- npm audit only reports packages that already have a published advisory; it does not inspect code, so brand-new malware passes it.
- The highest-risk part of any npm package is its preinstall, install and postinstall scripts, which run automatically with your user's permissions.
- Always inspect the published tarball, not the GitHub repository, because what is on the registry can differ from the source repo.
- Installing with --ignore-scripts blocks install-time payloads but not malicious code that runs when the package is imported.
- Red flags include eval or new Function on decoded strings, child_process calls, reads of process.env combined with network requests, and large base64 or hex blobs.
Most malicious npm packages are not clever. They are a few dozen lines that read environment variables or files like ~/.npmrc and ~/.ssh/id_rsa, then send them to a remote server, usually from a lifecycle script so the code runs the moment you type npm install. The hard part is not understanding the payload, it is finding it among thousands of transitive dependencies. Below is a checklist that works for a single package, followed by how to scale it to a whole lockfile.
Step 1: Look before you install
Everything in this section uses registry metadata only. Nothing is executed.
Check the lifecycle scripts
npm runs preinstall, install and postinstall for every package in the tree, including transitive ones. (If a package ships a binding.gyp and no install script, npm implicitly runs node-gyp rebuild.) Look at them first:
npm view some-package scripts npm view some-package@1.4.2 scripts --json
A legitimate install script usually compiles a native addon or downloads a prebuilt binary from a well-known host. Treat any of these as a reason to dig further:
node index.jsornode setup.jsin a package that has no native code- Inline shell:
curl ... | sh,wget,powershell -enc - A script that was added in a patch release when previous versions had none
Our guide on npm install scripts security covers the hooks in detail.
Check who published it and when
npm view some-package maintainers npm view some-package time --json npm view some-package dist-tags npm view some-package repository.url
Things that should make you slow down:
- A new maintainer on a long-lived package, especially right before a release. event-stream was compromised in 2018 after its original author handed publishing rights to a stranger who asked for them.
- A release after years of silence, or several releases within minutes. Account takeovers tend to push multiple versions quickly to hit every semver range.
- A version on the registry that has no matching tag or commit in the repository.
- A very young package with a name close to a popular one.
Check for typosquats and confusable names
Typosquats rely on a single slip: crossenv instead of cross-env, lodahs, electorn, or an unscoped copy of a scoped package. Compare the exact name in your package.json against the project's own documentation, and check weekly downloads on the registry page. A package with 40 downloads a week and a name one letter away from a package with 40 million is almost never what you meant.
Preview what will actually be shipped
npm pack some-package@1.4.2 --dry-run
This lists every file in the published tarball and its size. Look for files that do not belong: a minified .js file of several hundred kilobytes in a tiny utility, binaries, .node files in a package that claims to be pure JavaScript, or files with random names.
Step 2: Download the tarball and read it
Do this in a throwaway directory or container, not inside a project. npm pack downloads the tarball without running any scripts:
mkdir /tmp/inspect && cd /tmp/inspect npm pack some-package@1.4.2 tar -xzf some-package-1.4.2.tgz cd package cat package.json
Now grep for the patterns that show up in nearly every real-world payload. None of these are malicious on their own, but they are where you should spend your reading time:
# dynamic code execution
grep -rnE "eval\(|new Function\(|vm\.runIn|require\(['\"]vm['\"]\)" .
# spawning processes
grep -rnE "child_process|execSync|spawn\(|execFile" .
# decoding hidden strings
grep -rnE "Buffer\.from\([^)]*(base64|hex)|atob\(|fromCharCode" .
# secrets and credentials
grep -rnE "process\.env|\.npmrc|\.ssh|id_rsa|\.aws/credentials|Local State|Login Data|wallet" .
# network
grep -rnE "https?\.request|fetch\(|axios|net\.connect|dns\.lookup|WebSocket" .
# long encoded blobs (likely obfuscation)
grep -rnoE "[A-Za-z0-9+/=]{200,}" . | head
The combination is what matters. process.env in a config library is normal. process.env being serialized with JSON.stringify and sent in an HTTP request from a postinstall script is the single most common npm malware pattern. Likewise, a Buffer.from(x, 'base64') whose result flows into eval or new Function has very few legitimate uses.
Also check for code that runs on require, not just install. Read the file named in main, exports or module in package.json and look at the top-level statements. A payload that runs at import time survives --ignore-scripts. Browser-targeted malware, like the wallet drainer injected into chalk and debug in September 2025, often hooks fetch, XMLHttpRequest or window.ethereum rather than touching the filesystem.
Compare the tarball with the repository
The registry does not verify that a tarball was built from the linked repository unless the package has provenance. Diff them:
git clone https://github.com/owner/some-package repo cd repo && git checkout v1.4.2 diff -r --exclude=.git --exclude=node_modules . /tmp/inspect/package
Expect some differences (build output, missing test files). Unexplained new files or modified entry points are a strong signal. You can also diff two published versions directly with npm, which is the fastest way to see what changed in a suspicious patch release:
npm diff --diff=some-package@1.4.1 --diff=some-package@1.4.2
Check provenance and signatures
npm audit signatures
This verifies registry signatures for everything in your installed tree and reports which packages have provenance attestations, meaning they were built and published from a known CI workflow. Provenance does not prove code is safe (a compromised workflow still produces valid attestations), but a popular package that has always shipped with provenance and suddenly publishes a version without it deserves a look.
Step 3: Install defensively
npm install some-package --ignore-scripts # or for the whole project, permanently npm config set ignore-scripts true
Skipping scripts stops install-time payloads, which is where most npm malware lives. It will break packages that genuinely need to build native code; for those, run their build explicitly after reviewing them (npm rebuild some-native-pkg). Pin exact versions and commit your lockfile so a new malicious release cannot slip in through a ^ range on the next install.
What existing tools catch, and what they miss
| Tool | What it checks | Catches new malware? |
|---|---|---|
npm audit | Your tree against the GitHub Advisory Database | Only after an advisory is published |
| Dependabot, osv-scanner | Known CVEs and malicious-package advisories (OSV) | Only after it is reported |
npm audit signatures | Registry signatures and provenance | No, it checks origin, not behavior |
| Behavioral / source analysis | The code itself: scripts, network, eval, file access | Yes, that is the point, with some false positives |
Advisory databases are essential, but malicious packages are usually live for hours or days before anyone files a report. During that window, the only thing that can catch them is reading the code. See npm audit vs source code scanning for a fuller comparison.
Step 4: Automate the review across your lockfile
The manual checklist works for one package. A typical React or Next.js app has 800 to 1,500 packages in its lockfile, and no one reads all of them. This is the gap Togoder Security is built for. You upload a lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock, or the equivalent for PyPI, crates.io, Go, RubyGems and Packagist) and the service:
- Downloads the exact published artifact for every resolved version.
- Runs a cheap triage model over each file to clear obviously benign code such as type definitions and plain utility functions.
- Sends everything else to a full LLM review that looks for the same things as the grep list above, but in context: install-time payloads, credential and wallet theft, exfiltration, obfuscation and decode-then-eval chains, reverse shells, spawned processes, writes outside the package directory, drainers and miners.
- Caches results by the SHA-256 of each file's contents, so files anyone has scanned before cost nothing.
Parsing and price quotes are free; you pay per scan in USDC or use a monthly plan with API keys. You can also look up existing public reports without scanning anything, for example express, lodash or axios, and browse packages we have flagged on the malicious packages tracker. How the review works, and its limits, is documented on the methodology page.
AI review is guidance, not a guarantee. It can flag legitimate code that looks suspicious (installers that download binaries are a common false positive) and it can miss well-hidden logic. Treat a finding as a pointer to the exact file and lines worth reading yourself.
Quick checklist
npm view pkg scripts: any install hooks? Do they make sense?npm view pkg maintainers time: new maintainer, sudden release, burst of versions?- Is the name exactly right? Compare downloads with the package you meant.
npm pack --dry-run: unexpected files, binaries, huge minified bundles?- Grep the extracted tarball for eval, child_process, base64 decoding, process.env plus network.
npm diffagainst the previous version and diff against the repo tag.- Install with
--ignore-scripts, pin versions, commit the lockfile. - Scan the whole lockfile, not just the package you added.
Frequently asked questions
Does npm audit detect malware?
Only malware that has already been reported. npm audit compares your dependency tree against the GitHub Advisory Database, so a malicious version published an hour ago will pass until someone files an advisory for it.
Is it safe to run npm install to check a package?
No. npm install runs lifecycle scripts immediately, which is exactly where most npm malware executes. Use npm view and npm pack to inspect metadata and the tarball without running any code.
Does --ignore-scripts make installing a malicious package safe?
It blocks install-time payloads, which covers most known npm malware, but code that runs when the package is required or bundled into a browser app still executes. Pair it with a review of the package's entry point.
How do I check every dependency, not just the ones I added?
Scan the lockfile, which lists every transitive dependency at an exact version. Tools like osv-scanner check those versions against known advisories, and source-level scanners such as Togoder Security read the code of each file.
Can AI reliably tell if a package is malicious?
It is a strong filter but not a guarantee. LLM review catches patterns that signature tools miss, yet it can produce false positives on legitimate installers and false negatives on heavily hidden logic, so findings should be verified by a person.