Pular para o conteúdo
EdgeServers
Blog

nodejs

The npm supply chain in 2026 — lockfiles, sigstore, Socket, and the attacks we've actually seen

npm is the largest software supply chain in history and the most attacked. Here's the threat model in 2026 and the controls we ship on every managed Node.js stack.

26 de maio de 2026 · 10 min · por Sudhanshu K.

The npm supply chain in 2026 — lockfiles, sigstore, Socket, and the attacks we've actually seen

The npm registry serves something like 250 billion downloads a month. A meaningful percentage of that traffic resolves to packages whose maintenance is essentially a single hobbyist with a personal email account and no 2FA enabled in 2019. This is, fundamentally, the security model most Node.js applications rest on. It is fragile, and the attackers know it.

The good news is that the controls available in 2026 are dramatically better than they were even two years ago. Lockfile discipline, npm provenance via sigstore, runtime SBOM tooling like Socket, deep static analysis from Snyk and Endor — these are all mature. The work is to actually wire them up. This is the supply chain hardening baseline we apply to every Node.js engagement.

The threats, ordered by what actually happens

In ten years of managing Node fleets, the incidents we've seen — or directly responded to — break down roughly as:

  1. Compromised maintainer credentials, malicious version published. The classic. event-stream, ua-parser-js, coa, rc, and many less famous. An attacker takes over a maintainer account, publishes a patch version with a backdoor, the world npm cis it within hours.
  2. Typosquatting and dependency confusion. Attacker publishes requests (with an s) hoping someone fat-fingers a requirements.txt port. Or publishes a package with the same name as your internal scoped package, banking on misconfigured registry resolution.
  3. Postinstall script abuse. Package looks innocuous, lifecycle script downloads a second-stage payload. Hard to catch in code review because the malicious code isn't in the package — it's fetched at install time.
  4. Build-time exfiltration. Malicious dep reads process.env during build, exfiltrates NPM_TOKEN, GITHUB_TOKEN, AWS metadata. The attacker now has your CI credentials.
  5. Slowly-introduced backdoor. Long-tail attack. Attacker gains commit access legitimately (good PRs over months), then lands a subtle change that's only obviously malicious in hindsight. xz was the famous example outside npm; the same playbook applies inside.

The first four have known controls. The fifth is genuinely hard, and the honest answer in 2026 is "you reduce the blast radius, you don't prevent the attack."

Lockfile discipline, properly understood

The package-lock.json is your last line of defence against most of (1) and (2). But only if you treat it correctly.

The rules we enforce:

Commit package-lock.json. Always. Yes, even for libraries — the test environment for the library should be reproducible. The argument that "library consumers shouldn't see your lockfile" is true at install time (npm respects dependencies in package.json for consumers), so commit the lockfile for your own CI without harming downstream installs.

Use npm ci, never npm install, in CI and production builds. npm install will update the lockfile to satisfy new ranges, silently. npm ci will fail if the lockfile doesn't match package.json, which is exactly what you want.

Pin direct dependencies tightly. Caret ranges (^1.2.3) are fine for libraries that take dependencies on you, but for your application's direct deps, consider exact pinning (1.2.3). The cost is a few more Dependabot PRs; the benefit is that npm ci produces the exact dependency tree you tested.

Audit the lockfile diff in every PR. Not just package.json, the lockfile. A PR that adds one direct dep can pull in 40 transitive deps; the only place to see all 40 is the lockfile diff. We have a CI step that fails any PR adding more than 5 new packages without an explicit "ack" label.

# .github/workflows/lockfile-review.yml (excerpt)
- name: Count new packages in lockfile
  run: |
    git diff origin/main -- package-lock.json | \
      grep -E '^\+\s+"node_modules/' | wc -l | \
      awk '{ if ($1 > 5) { print "Too many new packages: " $1; exit 1 } }'

Provenance and sigstore — the actually-new thing

The biggest change in the npm threat model since 2023 is npm provenance, built on sigstore. When a maintainer publishes a package from a CI environment with npm publish --provenance, npm records a signed attestation linking that tarball to a specific GitHub Actions (or GitLab CI, or others) workflow at a specific commit SHA.

What this means in practice: for the increasing share of popular packages that publish with provenance, you can verify that the tarball you downloaded was built from the commit it claims, by a CI run you can inspect, with no human-in-the-loop signing key that could leak.

We check provenance at install time:

npm audit signatures

This walks the installed tree, fetches the registry's attestations, and reports any package whose signature or provenance is missing or invalid. We run it as a CI gate on every build. For packages without provenance, you get a warning; for packages with broken provenance, you get a failure.

The catch: as of mid-2026, "popular packages publish with provenance" still means maybe 40-60% of the top 1000 by download volume. The long tail is far worse. So npm audit signatures is necessary but not sufficient — it tells you "the maintainer claims this build is reproducible from a known CI run," it doesn't tell you "the code is safe."

The static analysis layer: Socket, Snyk, GitHub's own

Three tools we use, slightly different niches:

Socket.dev specialises in the behavioural analysis of npm packages. They detect things like "this version of foo started reading process.env and making network calls, which it didn't do in the previous version." That's the right signal for catching compromised maintainer attacks — the malicious version almost always introduces new capabilities that the legitimate version didn't need. We have Socket wired into PR review on every customer repo where the customer wants it; the cost is modest, the value on a maintainer takeover is enormous.

Snyk is the more traditional vulnerability scanner — CVE database lookups, license compliance, fix advice. We run it in CI on every Node project, with a policy that any new HIGH/CRITICAL with a known fix must be addressed within 14 days for HIGH and 7 days for CRITICAL.

GitHub Dependabot + npm's own npm audit is the baseline. It catches the publicly-disclosed CVEs and proposes upgrade PRs. It misses the not-yet-disclosed and the behavioural changes. So it's a floor, not a ceiling.

The honest truth: the controls overlap. If you can only pick one, pick Socket for behavioural detection. If you can pick two, add Snyk for CVE coverage. Dependabot is free and comes with GitHub, so it's basically always on.

Postinstall scripts: just turn them off

The single highest-ROI hardening step:

npm config set ignore-scripts true

Or per-project:

npm ci --ignore-scripts

This disables preinstall, postinstall, prepublish, etc. for all packages. The dramatic majority of npm packages don't need lifecycle scripts to function — they're libraries, they execute their code at require() time, not install time. Postinstall scripts are mostly used by native module builds (node-gyp, esbuild, sharp) and by malware.

The workflow:

  1. Set ignore-scripts=true globally.
  2. For the small number of packages that legitimately need a postinstall (we audit ours and there are usually 4-8), maintain an explicit allowlist with a separate install step.
  3. Anything outside the allowlist that breaks gets a discussion before we add it.

We've done this on every managed Node fleet for two years now and the friction has been close to zero, while the attack surface from postinstall payloads is gone entirely. This is one of those rare security controls that's both cheap and effective.

What we've actually seen, anonymised

A grab-bag from the last 18 months across customers we manage Node.js operations for:

A backend service auto-updating a transitive dep to a typosquatted version. Renovate matched a version range on a parent package; the parent had loose ranges for a child; the child resolved to a typosquat that had been published 36 hours earlier. Caught by Socket flagging "new package, very low download count, network-related capabilities added." Build failed. No prod impact.

Compromised maintainer of a 2-million-weekly-download package. Attacker published a patch version that, on Linux only, executed a postinstall script attempting to exfiltrate AWS_* env vars. We had ignore-scripts=true. Build proceeded normally. Two days later the package was unpublished and a CVE was issued. Zero impact.

Internal-package dependency confusion. A customer had a private scoped package @customer/utils. Someone in the wider community published a public customer-utils package (no scope). A developer typoed an import, npm resolved it, the public package was malicious. Caught at PR review by the lockfile-diff CI step — "why are we adding a new package to the lockfile that's not in package.json?"

A build pulling in 380 transitive dependencies for a CLI helper. Not malicious, just a maintenance nightmare. We refactored to remove the helper. The relevant security finding is that the probability of one of those 380 being compromised eventually is high. Smaller dependency surfaces are inherently safer.

Sigstore provenance check catching a rebuilt tarball. Someone re-published a package outside CI (the maintainer was debugging locally). The new tarball had no provenance attestation while the previous version did. npm audit signatures flagged it. Turned out to be benign, but the maintainer was alerted and fixed their publish workflow.

The baseline we apply

For every customer Node app we run security operations on, the supply chain baseline is:

  • package-lock.json committed, npm ci everywhere, lockfile diff reviewed
  • npm config set ignore-scripts true, explicit allowlist for the postinstalls we permit
  • npm audit signatures in CI as a hard gate
  • Socket on every repo (or equivalent behavioural scanner) for PR-time analysis
  • Snyk or Trivy for CVE coverage with SLAs on remediation
  • Renovate for dependency updates, grouped by package type, with auto-merge only for patch versions of pinned packages
  • NPM_TOKEN and other secrets accessed via --mount=type=secret at build time, never ARG
  • A documented list of "deps with no maintenance in 18+ months that we depend on" — these get extra review

None of this is exotic. All of it is a couple of days of focused work for an existing project. The catch is that none of it gets done unless someone owns it.

If your Node project has 800 packages in node_modules and you can't tell me which of them have provenance, which run postinstall scripts, or which Socket would flag — that's the project that's going to have a bad week eventually. Reach out for a one-day supply chain audit; it pays back fast.

Sudhanshu K. is a Senior Security Engineer at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). She has personally written more npm advisories than she would prefer, and runs npm audit signatures reflexively at this point.