The npm Security Landscape: Supply Chain Attacks 2026
TL;DR
npm supply chain attacks are more sophisticated in 2026, but so are the defenses. The attack surface: your project has ~1,000 transitive dependencies on average. Any one of them can be compromised. The notable 2025 incidents (several major packages had malicious versions briefly published) highlighted that npm audit is insufficient — it only catches known vulnerabilities, not novel attacks. Socket.dev, lockfile security, and provenance attestation are the new defense stack.
Key Takeaways
- ~1,000 transitive deps — average production Node.js app in 2026
- 3 attack vectors — typosquatting, dependency confusion, account takeover
- npm audit = necessary, not sufficient — only catches known CVEs, not novel attacks
- Socket.dev — proactive scanning for suspicious package behavior
- npm provenance — verified build artifacts since npm 9 (2023)
- Lockfiles are security —
package-lock.jsonis your integrity guarantee
The Attack Vectors
1. Typosquatting
# Real packages vs malicious lookalikes
lodash → lodassh (typo)
express → expresss (extra s)
react-router → react-rouuter (extra u)
@types/node → @type/node (missing s)
# npm's detection has improved but new typos keep appearing
# Defense: install packages by copy-pasting exact names from docs
2. Dependency Confusion
# Attacker publishes public package with same name as your private package
# npm by default checks public registry first
# Example:
# Your company has private package: @acme/internal-utils (in private registry)
# Attacker publishes public package: @acme/internal-utils (on npm public)
# If npm prefers public registry → malicious package installed!
# Defense: use .npmrc to scope private packages to private registry
# .npmrc
@acme:registry=https://your-private-registry.acme.com
//your-private-registry.acme.com/:_authToken=${NPM_TOKEN}
3. Account Takeover
# Most common attack in 2024-2025:
# 1. Package maintainer's npm account is compromised (phishing, credential leak)
# 2. Attacker publishes malicious version with a legitimate-looking changelog
# 3. Packages with auto-update or loose version ranges get poisoned
# Real 2024 incident pattern:
# - Popular utility package had new maintainer added
# - Malicious code in v2.1.5 executed at install time (postinstall script)
# - Stole API keys from process.env
# - Active for 3 days before removal
The Defense Stack
1. Lock Your Dependencies
// package.json — pin exact versions for critical deps
{
"dependencies": {
"express": "4.18.2", // ✅ Exact version
"lodash": "^4.17.21", // ⚠️ Allows minor updates
"react": "~18.2.0", // ⚠️ Allows patch updates
"some-util": "*" // ❌ Any version — dangerous
}
}
# Commit your lockfile — it's a security artifact
# package-lock.json or yarn.lock or pnpm-lock.yaml
# Why lockfiles matter:
# package.json: "react": "^18.2.0"
# On fresh install without lockfile: could install 18.3.0 (if malicious version published)
# On fresh install WITH lockfile: installs exactly 18.2.0 (verified via SHA-512)
# Verify lockfile integrity in CI:
npm ci --ignore-scripts # Reads lockfile strictly, skips lifecycle scripts
# vs:
npm install # May update lockfile, runs all scripts
# npm ci flags for security:
npm ci # Strict lockfile, no lockfile updates
npm ci --ignore-scripts # Also skips postinstall scripts
npm ci --audit # Also runs audit check
# pnpm equivalent:
pnpm install --frozen-lockfile # Like npm ci
2. npm Audit (The Baseline)
# Run audit regularly
npm audit
# Output example:
# found 3 vulnerabilities (1 moderate, 2 high)
# Run `npm audit fix` to fix them, or `npm audit` for details.
# Fix automatically
npm audit fix
# Check what's fixable
npm audit --json | jq '.vulnerabilities | to_entries[] | {name: .key, severity: .value.severity, fixAvailable: .value.fixAvailable}'
# Understand what audit DOESN'T catch:
# ❌ Novel malware (not yet in CVE database)
# ❌ Malicious behavior in legitimate-looking code
# ❌ Packages published 5 minutes ago
# ❌ Behavior based on environment (prod vs dev detection)
3. Socket.dev (Proactive Scanning)
# Socket analyzes package BEHAVIOR, not just CVEs
# Detects: suspicious network calls, env var access, install scripts, etc.
# Install the CLI
npm install -g @socketsecurity/cli
# Scan before installing a new package
npx @socketsecurity/cli report create react-router
# Output:
# ✅ No telemetry detected
# ✅ No suspicious network requests
# ✅ Package provenance verified
# ⚠️ 2 transitive dependencies with moderate risk
# GitHub App: integrates into PR checks
# Every new dependency gets automatic risk assessment
4. npm Provenance
# npm provenance (since npm 9, 2023) — proves where a package was built
# Packages published with provenance have a verified build chain:
# GitHub Actions → npm → package
# Check if a package has provenance
npm view react --json | jq '.dist.attestations'
# For your own packages, enable provenance in GitHub Actions:
# .github/workflows/publish.yml
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for provenance
steps:
- uses: actions/setup-node@v4
with:
registry-url: 'https://registry.npmjs.org'
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Result: package.json on npm shows "provenance: verified"
5. Restrict Install Scripts
# Disable postinstall scripts by default
# .npmrc
ignore-scripts=true
# Or only for CI:
npm install --ignore-scripts
# Downside: some packages require scripts (node-gyp builds, etc.)
# Solution: audit scripts before enabling
# List packages with install scripts:
npx @npmcli/map-workspaces
# Or: cat package-lock.json | jq '.packages | to_entries[] | select(.value.scripts != null) | .key'
Package Health as a Security Signal
Download trends and maintenance health correlate with security:
High-risk package profiles:
- Downloaded < 100/week (low visibility = slow vulnerability detection)
- Last published > 2 years ago (no security patches)
- Single maintainer, no org (single point of failure)
- Zero issues/PRs activity (abandoned)
Lower-risk profiles:
- Downloaded > 1M/week (fast community detection of issues)
- Published in last 3 months
- Multiple maintainers, organization-owned
- Active GitHub Issues/Discussions
PkgPulse health score incorporates:
- Last publish date
- Maintainer count
- Issue velocity
- Download trend
The Minimal Dependency Principle
The most secure dependency is one you don't have:
# Before installing: ask "can I implement this without a dependency?"
# Replace with native (2026 Node.js builtins cover a lot):
crypto operations → node:crypto (built-in)
file hashing → node:crypto createHash
HTTP requests → node:fetch (Node.js 18+)
URL parsing → URL class (built-in)
Path operations → node:path (built-in)
Queue/semaphore → simple implementation (no dep needed)
UUID generation → crypto.randomUUID() (Node.js 19.6+)
Base64 → Buffer.from(str).toString('base64') (built-in)
# For each dependency you add: what's the attack surface tradeoff?
Security Checklist for Production
□ Lockfile committed and verified in CI (npm ci)
□ npm audit passes (0 high/critical)
□ Socket.dev scanning on PRs
□ --ignore-scripts in CI unless specific packages need it
□ Private packages scoped to private registry in .npmrc
□ .npmrc committed (with no secrets — use environment variables)
□ Provenance enabled for packages you publish
□ Regular `npm outdated` review (30-day cadence)
□ npm 2FA enabled for publish operations
□ Monitor GitHub Security Advisories for your dependencies
The Attack Landscape: What Actually Happened in 2025-2026
The pattern of supply chain attacks has shifted meaningfully over the past two years. While direct maintainer compromise still occurs, attackers have increasingly favored account takeovers and systematic typosquatting campaigns — lower technical barrier, higher scale.
Typosquatting has become more sophisticated. Rather than simple character transpositions, attackers now research which packages are most commonly mistyped by analyzing autocomplete behavior in popular editors and common copy-paste errors from documentation. express packages with common character transpositions, scoped packages that mimic organization names with slight variations, and packages that mimic popular names by swapping similar-looking characters (zero vs letter O, one vs letter l) are all live attack surfaces. npm's security team processes hundreds of typosquatting reports per month, but the volume means new malicious packages can remain live for hours to days.
The account takeover vector is currently the most impactful. Maintainer accounts secured with only a username and password — no 2FA — are primary targets in credential stuffing campaigns that repurpose breached login databases from unrelated services. When a high-download package maintainer's npm account is compromised, a malicious version can be published within minutes. The detection time for supply chain attacks on npm averages 48 hours for high-visibility packages and can stretch to weeks for long-tail packages that fewer people are actively monitoring.
The response from npm has been substantive. npm's package provenance feature, introduced in npm 9, provides cryptographic assurance that a published package artifact came from a specific source repository and CI build environment via SLSA provenance attestations. A package published with provenance cannot be secretly rebuilt from different source code — the attestation links the artifact to a specific GitHub Actions workflow run, commit SHA, and repository. For consumers, npm audit signatures verifies that installed packages have valid registry signatures. Packages with full provenance go further, enabling consumers to trace a published artifact back to its exact source.
The Defense Stack That Covers the Real Threats
Supply chain defense is not a single tool — it's a layered approach where each layer covers the gaps of the others.
Layer 1 is lockfiles. package-lock.json or pnpm-lock.yaml pins the exact versions of all transitive dependencies, verified by SHA-512 integrity hashes. Without a committed lockfile, npm install on a fresh deployment might resolve to a different version than what was tested locally. CI should always use npm ci rather than npm install — npm ci reads the lockfile strictly, fails if the lockfile is out of sync with package.json, and never writes lockfile updates. This ensures that what runs in production is byte-for-byte what was tested.
Layer 2 is package provenance checking. Running npm audit signatures after install verifies that every installed package has a valid cryptographic signature from the registry. Packages with provenance go further, providing a verifiable chain from source code to published artifact.
Layer 3 is Socket.dev. Where npm audit only covers known CVEs in the advisory database, Socket monitors packages for behavioral signals that precede CVE assignment: new network calls added in a version, new file system access, obfuscated code, new environment variable reads. These behavioral anomalies are often the earliest detectable signal of a compromised package, appearing days or weeks before a CVE is formally assigned.
Layer 4 is restricting install scripts. Setting ignore-scripts=true in .npmrc for CI environments prevents postinstall script execution during dependency installation. Many supply chain attacks deliver their payload via postinstall — disabling this entirely eliminates the attack vector, at the cost of needing to manually enable scripts for packages (like node-gyp-based native modules) that legitimately require them.
Layer 5 is minimizing the production dependency surface. Running npm ci --omit=dev in production container builds installs only dependencies, not devDependencies, typically reducing the installed package count by 50–70%. Every package excluded from production is a package that cannot be exploited in production.
How Supply Chain Attacks Differ from Traditional Vulnerabilities
The mental model most developers have for npm security comes from the CVE-and-patch cycle: a vulnerability is discovered in a package, reported, assigned a severity score, and then fixed in a subsequent release. npm audit is built around this model — it compares your installed versions against a database of known CVEs and flags matches. This model works well for traditional vulnerabilities, where the attack requires an adversary to exploit a flaw in the code as-written.
Supply chain attacks operate on a fundamentally different premise. The adversary is not exploiting a flaw in the original code. They are replacing the legitimate code — or inserting new code alongside it — through the publication mechanism. The package appears to be what you expect: same name, same organization, similar version number. The malicious behavior runs alongside the legitimate functionality, often only on specific operating systems or environments, and is designed to evade casual inspection. Because the malicious code wasn't present when the CVE database was populated, npm audit reports zero issues — the attack is invisible to the tool developers rely on most.
The event-stream incident in 2018 demonstrated this pattern clearly. Dominic Tarr, the package's original maintainer, transferred ownership to an unknown contributor who had expressed interest in maintaining it. The new maintainer published a version that added a new dependency — flatmap-stream — which contained encrypted, obfuscated code that activated only in environments where a specific Bitcoin wallet application was installed. The malicious code was designed to steal private keys from that wallet. The package had 2 million downloads per week. The attack was active for two months before detection. npm audit would have shown nothing unusual, because the package had no CVEs — it was a novel attack delivered through a legitimate-seeming maintenance transfer.
The ua-parser-js account takeover in 2021 followed a similar structure. The package's maintainer had their npm account compromised via credential stuffing. The attacker published three malicious versions that ran cryptomining malware and credential-stealing code as postinstall scripts. The package had 7 million weekly downloads at the time. This is the attack that most directly illustrates why npm audit is insufficient: the malicious versions had no CVE, and the audit report would have been clean during the window when the malicious versions were published.
Provenance Attestation: Verifying the Build Chain
npm's provenance attestation system, introduced in npm 9, addresses a specific gap in the trust chain: even if you trust a package's source repository and its maintainers, you previously had no way to verify that the artifact published to the npm registry was built from that source without modification. A maintainer's machine could have been compromised, a CI system could have been altered, or a build artifact could have been manually modified before publication. Provenance closes this gap by linking published artifacts to a specific CI build run via SLSA (Supply-chain Levels for Software Artifacts) attestations.
When a package is published with provenance using npm publish --provenance, npm records a set of attestations alongside the package artifact: the source repository URL, the commit SHA, the CI workflow file path, and a cryptographic signature from the CI provider's identity system. GitHub Actions, which is the primary supported environment, uses its OpenID Connect identity to sign the attestation. The result is a verifiable chain: the artifact on npm cryptographically proves it was built from commit abc123 of github.com/org/repo using workflow .github/workflows/publish.yml. An attacker who compromised a maintainer's laptop after the CI build would need to also compromise GitHub's attestation infrastructure to forge a valid provenance record.
Verifying provenance for packages you install is possible via npm audit signatures, which checks that every installed package has a valid registry signature, and npm view <package> --json which shows attestation details for packages published with provenance. In practice, most developers don't verify provenance manually — instead, tools like Socket.dev surface this information in their automated scanning output, flagging packages that lack provenance as a risk signal. For packages you publish, enabling provenance is a low-effort security improvement: it requires only the id-token: write permission in your GitHub Actions workflow and the --provenance flag on npm publish.
The ecosystem adoption of provenance has grown steadily since 2023. Major packages in the React, Node.js, and tooling ecosystems have added provenance to their publish workflows. The absence of provenance is increasingly a notable signal for security-conscious consumers: a high-download package that hasn't added provenance in 2026 is either maintained by someone unfamiliar with the feature or is still relying on a build process that doesn't support it. Neither is reassuring.
Internal Proxy Registries as Defense in Depth
Running an internal npm proxy registry — Verdaccio, Artifactory, or Sonatype Nexus — adds a controlled chokepoint between your development environment and the public npm registry. Rather than developers installing packages directly from registry.npmjs.org, all requests pass through an internal registry that can enforce policies, cache approved versions, and prevent new packages from being installed without review.
The dependency confusion defense is the most direct benefit. Scoped private packages configured to resolve through the internal registry cannot accidentally resolve to public npm — the proxy handles the routing based on package scope. A package named @yourcompany/internal-utils can be configured to only ever be served from the internal registry, making a dependency confusion attack on that package impossible regardless of whether a malicious package with the same name is published to public npm.
Version pinning and curation is a second benefit. An internal registry can be configured to mirror only approved versions of external packages — so even if a malicious version is published to public npm, developers on your network cannot install it until a human reviews and approves the new version. This creates an approval gate for dependency updates that is especially valuable for large teams where multiple developers have the ability to run npm install. The tradeoff is operational overhead: the registry needs to be maintained, mirrors need to be kept current for security patches, and the approval process needs to be fast enough not to block developers on urgent fixes.
The performance benefit is a side effect worth mentioning: caching packages in an internal registry dramatically reduces install times in CI environments where the same dependencies are installed repeatedly across many build jobs. For teams running hundreds of CI jobs per day, the difference between fetching packages from npm's servers versus a local registry mirror is significant both in time and in external network cost.
For smaller teams, the operational cost of running an internal registry may not be justified. In that case, the .npmrc scoping approach — configuring private package scopes to resolve through a private registry service like GitHub Packages or npm's own organization scoping — provides the most critical defense (dependency confusion prevention) without the infrastructure overhead.
Lockfile Integrity in CI: The Verification Step Most Teams Skip
The lockfile is the most undervalued security artifact in a JavaScript project. package-lock.json and pnpm-lock.yaml record the exact resolved version and integrity hash (SHA-512) of every installed package across the entire dependency tree — not just direct dependencies, but every transitive package. This means that even if a malicious version of a package is published to npm between your last npm install and your next CI run, npm ci will refuse to install it because the version and hash don't match what's in the lockfile.
This guarantee only holds if the lockfile is both committed to version control and strictly enforced in CI. Many teams commit the lockfile but then use npm install (which can update it) rather than npm ci (which fails if the lockfile doesn't match) in their CI pipelines. This creates a gap: the lockfile in the repository may reflect a safe state, but the CI install may silently resolve to a different set of packages if the lockfile has diverged from package.json. The correct CI command is always npm ci (or pnpm install --frozen-lockfile), never npm install.
Lockfile PRs should be reviewed with the same attention given to application code changes. When Dependabot or Renovate opens a PR that updates a dependency, the lockfile diff shows exactly which transitive packages changed, their old and new versions, and their integrity hashes. Reviewing this diff for unexpected changes — a transitive package updated when only a direct dependency was bumped, a package added that wasn't there before — catches cases where a dependency update pulls in additional packages that weren't expected. Most such changes are benign, but the practice of looking at the full lockfile diff rather than just the package.json diff develops the habit of awareness that catches the non-benign cases.
The npm ci --ignore-scripts flag adds a second layer by refusing to execute any preinstall, install, or postinstall scripts during the install process. Because many documented supply chain attacks (including ua-parser-js, node-ipc, and several undisclosed incidents) delivered their payload through install-time scripts, disabling scripts by default in CI removes the most common delivery mechanism. The downside is that native module packages (those using node-gyp) require their build scripts to run — these need to be explicitly allowlisted, which is itself a useful security practice as it forces awareness of which packages need native compilation and why.
The Role of 2FA and npm Access Policies in Account Takeover Prevention
Account takeover remains the highest-volume attack vector for npm supply chain compromises, and the primary defense at the individual maintainer level is two-factor authentication. npm's security team has pushed hard on this: since 2022, npm has required 2FA for any account that publishes high-impact packages (those with more than 1 million weekly downloads). For all other packages, 2FA is strongly recommended and trivially enabled through account settings. Despite this, credential-stuffing campaigns continue to find accounts without 2FA, because mandatory enforcement covers a threshold that many impactful-but-not-high-visibility packages fall below.
For teams publishing packages, npm's granular access controls provide a second layer. Publish tokens can be scoped to specific packages and restricted to automation-only use (preventing interactive publishing from developer machines). The npm token create --cidr-whitelist flag restricts a token to publishing only from specific IP ranges — ensuring that even a stolen token cannot be used from an attacker's infrastructure. Combining automation-only tokens with provenance attestation creates a situation where a stolen token cannot be used to publish without a corresponding verifiable CI build, significantly raising the cost of a successful account takeover attack.
The organizational practice of regularly auditing which accounts have publish access to your packages is frequently skipped. Developers who leave teams often retain npm publish access because there's no offboarding checklist item to remove it. Running npm access list collaborators <package> shows every account with publish rights. Removing departed contributors from this list and rotating automation tokens annually is the kind of security hygiene that seems low-priority until a former employee's credentials appear in a breach dump.
Check package security signals on PkgPulse — health scores include maintenance and vulnerability data.
Compare React and Vue package health on PkgPulse.
See also: Security Vulnerabilities by Category and npm Supply Chain Security Guide 2026, npm Dependency Trees: Most Nested Packages 2026.
See the live comparison
View react vs. vue on PkgPulse →