Your App Is Only as Secure as the Code You Didn't Write.
Software supply chain attacks stopped faking packages and started taking over real ones. Bitwarden, SAP, TanStack and Red Hat all shipped poisoned versions of their own npm packages in 2026. What it costs a business, and the eight controls that limit your exposure.
Open the folder of any modern web application and count what's actually in it. We ran npm ls --all on this site: 20 direct dependencies, 507 installed packages. We chose 3.9% of them. The rest arrived transitively, picked by someone else, maintained by people we've never met.
That's a small project. Black Duck's 2026 OSSRA report, which audited 947 commercial codebases, puts the mean at 1,180 open source components per application, up 30% in a year.
Every one of those packages runs with the same permissions as your own code. Most founders have never thought about this. Most CTOs think about it constantly. 2026 has been the year that gap got expensive.
What changed
Supply chain attacks aren't new. What's new is the industrialisation. Typosquats still land, but they're no longer the interesting half of the problem. The growth is in attackers taking control of the packages developers already trust.
The 2025 template was to get hold of a maintainer's account, then republish every package it controls. The Shai-Hulud worm did that across roughly 500 npm packages by 18 September 2025, harvesting credentials from a postinstall script that ran TruffleHog across the filesystem and then republished itself using any npm token it found. A second wave in November backdoored around 800.
Through 2026 two things changed. First, the attacks got personal. On 31 March, an attacker published axios 1.14.1 tagged latest after a social engineering campaign put a remote access trojan on the lead maintainer's own computer. Both malicious releases added plain-crypto-js@4.2.1, a crypto-js lookalike whose postinstall hook dropped a cross-platform RAT. npm pulled them in under three hours, but axios is installed more than 100 million times a week. Note what didn't save it: axios already used npm's OIDC trusted publishing on its 1.x line. A publish from a hijacked account session sidesteps that by design, because it looks like the maintainer.
Second, and more often, the entry point moved upstream into the build pipeline. Four organisations shipped malicious versions of their own official packages this year. None were lookalikes.
- Bitwarden, 22 April:
@bitwarden/cli2026.4.0, live 93 minutes, roughly 334 downloads, via a compromised third-party GitHub Action in its release pipeline. Bitwarden's notice states it found no evidence that end-user vault data was accessed or at risk, and that the legitimate CLI codebase was not affected. - SAP, 29 April: four SAP-maintained packages, including
@cap-js/sqliteandmbt. SAP's own root cause was a release workflow that held publish permissions with no manual approval gate, hijacked by commits pushed to a non-main branch. - TanStack, 11 May: 84 versions across 42 packages in a six-minute publishing window, though npm didn't remove them for another three to four hours. TanStack's postmortem chains three flaws: a
pull_request_targetmisconfiguration, GitHub Actions cache poisoning across the fork trust boundary, and an OIDC token lifted from the runner's process memory. Only the Router and Start monorepo was affected; Query, Table and Form were not. Because the packages went out through the real workflow, they carried valid provenance attestations. - Red Hat, 29 May: 32
@redhat-cloud-servicespackages, after a GitHub account was compromised through a malicious VS Code extension. Red Hat's advisory RHSB-2026-006 states no product or enterprise software shipped a compromised version, no managed cloud service was impacted, and no customer action is required.
At the end of Q2 2026, Sonatype Research had logged more than 1.8 million malicious packages, with npm alone accounting for 96.6% of that quarter's count. ReversingLabs recorded a 73% rise in detections during 2025, alongside declines of 43% on PyPI and 60% on NuGet. It's careful about why: those declines coincided with security investment on those registries, and it reads the pattern as displacement, with attackers moving to platforms that have fewer controls.
The targets have narrowed too: CI runners, Actions workflows, publishing credentials, cloud secret managers, developer machines. The Nx compromise in August 2025 went further and conscripted the developer's own tooling, probing for locally installed AI CLIs and invoking each with its safety bypass to sweep the filesystem for wallets, keystores and .env files.
Why this matters more than it used to
Three things turned this from an engineering curiosity into a business problem.
Dependencies are deeper than ever. Across the Black Duck audit, only 7% of components were running their latest available version and 41% were ten or more versions behind. 92% of codebases carried something four or more years out of date. An unmaintained dependency with a known flaw is technical debt with a deadline attached.
Automation removed the human checkpoint. Code moves from commit to production without anyone reading the lockfile diff. In the four best-documented compromises, the malicious versions were pulled within hours of publication. That window is short, and an automated pipeline fits an entire release inside it.
AI-assisted development moves faster than review. Nobody audits a package an agent added on their behalf. The gap between "a package entered my project" and "anyone looked at it" is wider than it's ever been. The tools aren't at fault. The process around them usually is.
What this actually costs a business
The technical description of a supply chain attack is abstract. The business consequences aren't.
Credential theft cascades. The TanStack payload reached for AWS Secrets Manager, GCP metadata, Kubernetes service-account tokens and Vault tokens. Once an attacker holds those, the incident stops being about one bad package and becomes about everything the keys unlock.
Your compromise becomes your customers' compromise. If you publish packages, SDKs or images that others install, a poisoned dependency in your build becomes one in theirs. If they only integrate over an API, it becomes their data-exposure problem instead. Neither is a conversation you want to open.
Enterprise deals stall. Procurement tightened well ahead of the law. The EU Cyber Resilience Act's reporting obligations start on 11 September 2026, but its Annex I requirement for a machine-readable SBOM "covering at the very least the top-level dependencies" doesn't apply until 11 December 2027, and only to products placed on the market from that date. If you sell browser-accessed SaaS, the Commission's guidance puts you outside the CRA altogether. None of that stops a buyer's security questionnaire arriving today, and companies that can't answer questions about dependency provenance lose deals to companies that can, usually without learning why.
Incident response is slow. Discovering that a compromised package ran in your pipeline three weeks ago means auditing everything since, rotating every credential that might have been exposed, and notifying anyone affected. Small teams lose months.
What good practice looks like
The defences are well understood, mostly one line of configuration, and would have limited your exposure to most of 2026's incidents. None of this requires a security team. Note the asymmetry, though: seven of these eight protect you from a compromised package. They don't stop you from becoming one.
Commit your lockfile, and install from it in CI
npm ci is the enforcement step. It requires an existing package-lock.json, wipes node_modules first, and fails rather than quietly updating the lock when package.json disagrees with it. The equivalents are pnpm install --frozen-lockfile and yarn install --immutable. A committed lockfile plus a plain npm install in CI is not a pin.
Set a cooldown
Refuse to install versions published in the last few days. Poisoning campaigns are usually caught inside that window, and waiting costs you nothing.
| Tool | Setting | Where | Unit |
|---|---|---|---|
| npm | min-release-age | .npmrc | days |
| pnpm | minimumReleaseAge | pnpm-workspace.yaml | minutes |
| Yarn | npmMinimalAgeGate | .yarnrc.yml | duration |
| Dependabot | cooldown → default-days | .github/dependabot.yml | days |
Mind the units: 7 is a week in npm and seven minutes in pnpm. Defaults differ more than you'd expect. npm needs 11.10.0 or later and ships no cooldown by default, so nothing happens until you set it. pnpm has defaulted to 1440 minutes since v11, but that built-in default is deliberately non-strict and will fall back to a newer version rather than fail, so declare it explicitly. Yarn has defaulted to 1d since 4.15.0, and Dependabot to three days since July 2026. Dependabot's cooldown only delays the pull request it raises, so set this in the package manager too.
Pin your GitHub Actions to full-length commit SHAs
Git tags are mutable. In March 2025 an attacker rewrote every release tag on tj-actions/changed-files, v1 through v45.0.7, to point at one malicious commit that read the runner's process memory and printed the workflow's secrets into the build log. The action was referenced by thousands of repositories, but that's a usage count, not a casualty count: Endor Labs traced 614 repos that ran an affected workflow inside the window and 218 that actually leaked a secret. Workflows pinned to a SHA were unaffected. Per GitHub, a full-length commit SHA is still the only way to use an action as an immutable release, and you can now enforce it by policy.
Get standing secrets out of the pipeline
Replace them rather than scoping them. Give the job id-token: write and contents: read, then bind the cloud trust policy to the OIDC sub claim, ideally to an environment rather than a branch. npm revoked all classic tokens on 9 December 2025, npm login now returns a two-hour session token, and granular write tokens cap at 90 days. SAP's incident is the argument for binding narrowly: its publisher trust covered the whole repository rather than one workflow on a protected branch.
Turn off dependency install scripts, then allow-list what needs them
npm 12, released 8 July 2026, blocks dependency lifecycle scripts by default through a new allowScripts policy, reviewed with npm install-scripts ls and approve. Set strict-allow-scripts=true to make an unreviewed script fail the build rather than warn. ignore-scripts still defaults to false and is the blunter tool, killing your own scripts too. Yarn has disabled scripts by default since 4.14.0; pnpm 11 replaced onlyBuiltDependencies with allowBuilds. What breaks is anything compiling a binary at install time. Necessary, and no longer sufficient: campaigns have moved to preinstall and to module-load time to defeat script blocking entirely.
Scan for malware, not only for known vulnerabilities
npm audit and standard Dependabot alerts check your tree against advisories for flaws in legitimate packages. A credential stealer published 40 minutes ago has no CVE and passes clean. Malicious code needs its own check: Dependabot's malware alerts, the OpenSSF malicious-packages feed in OSV, or a behavioural tool like Socket that blocks at install time rather than at review time.
Generate a software bill of materials
An SBOM decides whether "which of our systems were exposed?" takes an hour or a fortnight. npm sbom --sbom-format=cyclonedx is the baseline, though it emits CycloneDX 1.5 while the current spec is 1.7. cdxgen, Syft and Trivy produce richer output. Ignore advice citing US Executive Order 14028 as the driver: OMB Memorandum M-26-05 rescinded the government-wide attestation requirement in January 2026, though agencies can still require an SBOM on request through contract terms.
Scope-lock your internal packages
Bind each internal scope to one registry in .npmrc: @myco:registry=https://reg.example.com, with auth on a separate line keyed by host. Commit the literal ${NPM_TOKEN} string, never the value. A scope only ever resolves to one registry, which closes dependency confusion, where an attacker publishes your internal package name publicly at a higher version and wins the resolution race.
One standard checklist item deserves demotion. Scheduled credential rotation does very little here: a worm that runs at install time reads live credentials and uses them within minutes, so a 90-day cycle never enters the picture. Rotate on evidence of compromise, and spend the effort on not holding standing credentials at all. The judgement call comes earlier anyway: treat maintainer count and release cadence as stack-selection criteria, not post-incident forensics.
The question founders should be asking
If you're not technical, you can't evaluate this directly. But you can ask, and the answers tell you a great deal.
How many packages does our application install, and how many did we choose? Nobody knows what all of them do. A good answer gives you both numbers and describes how the risk is managed anyway.
What can our CI pipeline reach, and does it need all of it? Almost every pipeline has more access than it needs.
What happens if a package we depend on is compromised tomorrow? You want to hear about a cooldown window, lockfile enforcement in CI, and malware scanning separate from vulnerability scanning. Not "that's unlikely."
Do we have an SBOM, and can we produce one on request? If the answer is no, that's a stalled enterprise deal waiting to happen.
What long-lived credentials still exist, and can we delete them? Since npm revoked classic tokens, the honest answer for publishing should be "none."
A team that answers these confidently is thinking about the whole system. A team that treats the questions as paranoia is telling you something too.
The uncomfortable part
There's no version of modern software development where you audit every line of code you depend on. Anyone promising otherwise is selling something.
What you can do is decide, deliberately, how much trust you extend and what happens when that trust is misplaced. The controls that decide how bad it gets are unglamorous, and none of them are purchases: a frozen lockfile, pinned actions, scoped permissions, and being able to answer "what did we install last Tuesday?" without a two-week investigation.
Security in a dependency-heavy world is less about prevention than containment. You will occasionally install something bad. The question is whether you find out in twenty minutes or six weeks, and whether that package had access to everything or to almost nothing.
This is the same problem we've written about before in a different costume: a vibe-coded MVP that looks finished but was never built to carry weight. It never feels urgent, and there's always a feature that matters more this week. But these practices take a few days to establish at the start of a project and considerably longer to retrofit onto two years of accumulated code.
As if to make the point: at 09:00 UTC this morning, a maintainer's compromised GitHub account was used to push a malicious preinstall hook into the keyv and cacheable packages, which then went out through GitHub Actions carrying valid build provenance. Wiz counted over 400 affected packages within hours. Figures in this post are current as of publication and this area moves weekly.
Not sure where you stand?
At Nortis, dependency hygiene, pipeline security and build provenance are part of how we set up every project, not a phase we get to later. If you want an honest read on your current exposure, or help putting the fundamentals in place, we'll tell you straight what's worth doing first.