Safeguard
Open Source Security

10 npm security best practices

Real npm supply-chain incidents from event-stream to the 2025 chalk/debug hack, and 10 concrete practices to stop install-time attacks, typosquatting, and token theft.

Priya Mehta
DevSecOps Engineer
7 min read

In September 2025, attackers compromised the npm account of a maintainer known as "qix" through a phishing email and pushed malicious versions of chalk, debug, ansi-styles, and 15 other packages that together account for more than 2 billion weekly downloads. The payload hijacked browser wallet transactions in real time. Two months earlier, a self-replicating worm dubbed Shai-Hulud spread through compromised npm packages, stealing credentials and republishing itself into every package a victim could access. These weren't edge cases — npm now hosts over 3.6 million packages, and the average JavaScript project pulls in 680+ transitive dependencies it never directly chose. Most teams have no visibility into that graph, no idea which packages actually execute in production, and no process for what happens when one of them turns hostile. Below are ten concrete practices that address the actual attack paths — install-time code execution, typosquatting, token theft, and unpatched transitive risk — not just the ones npm audit happens to flag.

What makes npm uniquely risky compared to other package ecosystems?

npm's install-time script execution is the single biggest structural risk in the ecosystem. Unlike Maven or most Go modules, npm packages can define preinstall, install, and postinstall hooks that run arbitrary code the moment npm install executes — before any application code runs, before any test suite catches anything. This is exactly how the ua-parser-js compromise worked in October 2021: version 0.7.29 shipped a postinstall script that dropped a password-stealing trojan and cryptominer on any machine that ran npm install in a project depending on it, and the package had roughly 7 million weekly downloads at the time. Combine that with npm's flat, deeply nested dependency trees — a single create-react-app scaffold has historically pulled in 1,300+ packages — and you get a massive, largely invisible execution surface. Practice #1 and #2: run npm install --ignore-scripts in CI and production builds by default, and use a tool that allowlists which packages are permitted to run install scripts at all (npm's own npm config set ignore-scripts true plus a manual allowlist for packages you've vetted, like node-gyp-dependent native modules).

How do you stop typosquatting and dependency confusion before they reach a build?

You stop them by verifying package provenance and namespace ownership before install, not after a scan flags it. Typosquatting relies on developers mistyping or misremembering package names — crossenv instead of cross-env, mongose instead of mongoose — and these fakes have sat on npm for months at a time collecting installs from CI pipelines with typo'd package.json entries. Dependency confusion is subtler: in 2021, security researcher Alex Birsan demonstrated that internal packages referenced by name only (no scope, no private registry pin) could be shadowed by a public npm package of the same name, tricking build systems at PayPal, Microsoft, and Apple into pulling attacker-controlled code instead of the internal one. Practices #3 and #4: scope every internal package (@yourcompany/utils, not utils), and configure .npmrc with explicit registry mappings per scope so there's no ambiguity about where a package resolves from. Pair this with a policy that blocks any first-time dependency addition until it's been checked against known-typosquat lists and published-package age (packages published in the last 7 days publishing to popular typo-adjacent names are a strong signal).

Is npm audit enough to manage dependency vulnerabilities?

No — npm audit tells you a CVE exists in your dependency tree, but it can't tell you whether your code actually calls the vulnerable function, which is the difference between a real incident and a false alarm. npm audit matches installed versions against the GitHub Advisory Database and will happily report a critical CVE in a deeply nested transitive dependency of a devDependency that never ships to production. Teams that triage every audit finding as equally urgent burn hours on non-issues: one widely cited stat from dependency-scanning research is that fewer than 15% of vulnerable functions flagged by version-based scanners are actually reachable from application code. Practice #5: treat npm audit output as a starting list, not a priority list, and layer reachability analysis on top so you're patching the CVE in minimist that your code path actually invokes, not the one buried three layers deep in a build-time tool. Practice #6: pin exact versions with package-lock.json (or npm-shrinkwrap.json for published libraries) and enforce npm ci instead of npm install in every CI pipeline, so builds are byte-for-byte reproducible and a compromised registry can't silently swap in a different tarball for the same version string.

What should you do about unmaintained or single-maintainer packages?

You should inventory them and set explicit risk tiers, because a package's popularity has no correlation with how well it's protected. The event-stream incident from November 2018 is the canonical example: a package with 2 million weekly downloads was handed off to a new "maintainer" who added a single malicious dependency, flatmap-stream, specifically to target the Copay Bitcoin wallet's private keys. The original maintainer, Dominic Tarr, had simply stopped actively working on it and accepted a pull request from someone he didn't know. In March 2022, node-ipc — a dependency of the popular unicorn toolchain — added "protestware" code that detected IP addresses geolocated to Russia or Belarus and overwrote files on disk. Both packages passed every vulnerability scanner right up until the moment they didn't. Practice #7: generate and maintain a software bill of materials (SBOM) so you know, at any point in time, exactly which packages, versions, and maintainers sit in your supply chain — and can react in minutes, not days, when the next event-stream happens. Practice #8: flag single-maintainer packages with low commit velocity for extra review before version bumps are auto-merged by tools like Dependabot or Renovate.

How do you keep CI/CD tokens and npm publish credentials from becoming the next breach?

You keep them safe by scoping tokens tightly and rotating publish credentials on a schedule instead of leaving them valid indefinitely. Compromised npm publish tokens are how attackers push malicious versions of legitimate packages without ever touching a maintainer's actual account — it's the mechanism behind both the coa/rc compromise in November 2021 and the September 2025 chalk/debug incident, where a phishing page impersonating npm's own login flow harvested a maintainer's 2FA-backed session token. Practice #9: enable npm's granular access tokens (scoped to specific packages, read-only where possible) instead of classic tokens that can publish to anything the account owns, and require hardware-key-based 2FA (not SMS or TOTP) for any account with publish rights. Practice #10: audit CI environment variables for long-lived NPM_TOKEN values sitting in plaintext secrets stores, and replace them with short-lived, workflow-scoped credentials (GitHub Actions OIDC-to-npm trusted publishing, launched in 2025, eliminates the static token entirely for many pipelines).

How Safeguard Helps

Safeguard maps your actual npm dependency graph and runs reachability analysis to tell you which of the CVEs in your npm audit output are triggered by code paths your application really executes — cutting through the noise instead of adding another list of red flags. Griffin, Safeguard's AI analysis engine, reviews new and updated package versions for behavioral anomalies — surprise install scripts, obfuscated payloads, unexpected network calls — the same patterns present in the ua-parser-js and chalk compromises, before they land in a build. Safeguard generates and ingests SBOMs automatically on every build so you have a continuously accurate inventory of what's actually in your supply chain, not a point-in-time snapshot from last quarter's audit. When a fix is available, Safeguard opens auto-fix pull requests with the minimal version bump needed to remediate the reachable vulnerability, tested against your lockfile, so remediation is a merge instead of a manual dependency-chasing exercise.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.