Software Supply Chain Attacks: How They Work & Defenses

Most breaches you read about did not start with someone attacking your code. They started with someone attacking a dependency, a build step, or a registry account you trusted. That is the software supply chain, and it is now one of the most productive ways in.

The uncomfortable truth is that a modern service ships thousands of transitive packages, base image layers, and CI plugins that you never wrote and rarely read. Any one of them runs with your build's privileges. This post walks the attack landscape and the defenses that hold up. For the deep dive on proving what shipped, see our companion post on supply-chain security with SBOMs and attestation.

The attack landscape #

Typosquatting and slopsquatting. The oldest trick is registering a package whose name is one keystroke from a popular one (reqeusts, loadash). Slopsquatting is the newer, nastier variant: AI coding assistants confidently hallucinate package names that do not exist, and attackers pre-register those exact names. A developer pastes the suggestion, npm install succeeds, and malware lands. The hallucinated names are surprisingly stable across prompts, which makes them cheap to farm.

Dependency confusion. If your build resolves package names across both a public registry and a private one, an attacker who learns an internal name (say acme-internal-auth) can publish a public package with the same name and a higher version. Naive resolvers prefer the public one. Alex Birsan's 2021 research turned this into shipped code inside dozens of large companies without a single stolen credential.

Hijacked maintainer accounts and malicious updates. A package can be perfectly safe for years, then turn. The event-stream incident is the archetype: a maintainer handed off a popular library, and the new owner slipped in code targeting a specific downstream wallet app. Account takeover through phishing or a leaked token produces the same result, a malicious version published under a trusted name.

Malicious install scripts. npm postinstall and equivalents run arbitrary code at install time, before any of your tests or review. This is the payload delivery mechanism for most of the attacks above. The dangerous window is between resolving a dependency and running it.

Build-system and CI/CD compromise. Your pipeline holds cloud credentials, signing keys, and publish rights, which makes it a higher-value target than any single package. Poisoned pipeline execution, a malicious pull request that alters the build definition, or a stolen long-lived token lets an attacker publish tainted artifacts that look completely legitimate.

Compromised base images. A FROM line pulls in an OS, its package manager, and whatever the image author included. A backdoored or simply stale base image inherits straight into production.

The pattern behind the headline cases (the xz backdoor's patient, social-engineered maintainer takeover included) is consistent: attackers move upstream, where one compromise fans out to every consumer. AI is scaling both ends of this. It generates plausible package names to squat and lowers the effort to produce convincing malicious packages and pull requests at volume.

Defenses #

No single control covers this. You need layers across install, build, and publish.

Pin and lock with hashes. A lockfile that records exact versions and cryptographic hashes means a swapped artifact fails the install rather than executing. Install from the lockfile, not from loose ranges:

bash.bash

# Node: install exactly what the lockfile pins, and skip lifecycle scripts
npm ci --ignore-scripts

# Python: fail if a resolved artifact's hash is not in the lockfile
pip install --require-hashes -r requirements.lock

code

# requirements.lock — hashes make substitution detectable
requests==2.32.3 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6

Block dependency confusion with namespaces. Route installs through a private registry, and reserve your organization's scope so nobody can publish under it publicly. Pin the scope to your internal registry only:

code

# .npmrc — @acme resolves ONLY from the internal registry
@acme:registry=https://npm.internal.acme.com/
registry=https://registry.npmjs.org/

Disable install scripts and review new dependencies. Default to --ignore-scripts and allowlist the few packages that genuinely need lifecycle hooks. Treat a brand-new dependency or a jump to a fresh maintainer version as a change that gets human eyes, not an automatic merge.

Scan continuously. Software composition analysis flags known-vulnerable and known-malicious packages before they merge and as new advisories land. Wire it into the pipeline and pull requests, covered in our dependency and SCA scanning guide.

Least-privilege CI with short-lived tokens. Replace static registry and cloud secrets with short-lived OIDC tokens minted per job. Scope each job to exactly what it needs, and never expose publish credentials to pull requests from forks. A stolen token that expires in minutes is a far smaller prize.

Sign and verify artifacts, generate provenance. Sign what you publish and verify signatures on what you consume, so a package that did not come from your build is rejected. Generate an SBOM and build provenance so you can answer "what is in this, and where did it come from" during an incident.

Priorities #

Sequence the work by leverage:

Lockfiles with hashes plus --ignore-scripts by default. Cheap, and it neutralizes the most common install-time payloads.

Private registry with scope reservation. Closes dependency confusion outright.

SCA scanning in CI and on pull requests, blocking on known-malicious.

OIDC short-lived tokens; delete long-lived publish secrets.

Artifact signing plus verification, then SBOM and provenance for response.

The call we'd make #

Start at the install boundary, because that is where the highest-volume, lowest-effort attacks land, and it is the cheapest thing to fix this week. Lockfile hashes, disabled scripts, and a reserved namespace shut down typosquatting, slopsquatting, dependency confusion, and most malicious postinstall payloads with configuration you already control.

Then move up to the build. Kill static publish secrets in favor of OIDC, and treat CI permissions as the crown jewels they are. Signing, SBOMs, and provenance come last not because they are optional, but because they pay off most once the noisy front-door attacks are already closed. Fold all of it into your broader application security best practices rather than running it as a side project. The attackers are automating; your defenses have to be defaults, not heroics.