Software Supply Chain Attacks: How They Work & Defenses
A practical tour of how software supply chain attacks reach your build, and the controls that actually stop them.
Key takeaways
- A software supply chain attack compromises something you trust, such as a dependency, registry account, base image or CI pipeline, instead of your own code.
- Stop most of them at install time with hash-pinned lockfiles, blocked install scripts and reserved package scopes, then replace static CI secrets with OIDC and sign what you ship.
On this page
A software supply chain attack compromises something your build trusts (a dependency, a registry account, a base image, a CI pipeline) rather than your own code. The cheapest defenses sit at install time: lockfiles with hashes, install scripts blocked by default, and a reserved package scope. Then remove static CI secrets and sign what you publish.
Verified against
npm CLI 12.2.1 and 11.21.1, pip 26.2 docs, against docs.npmjs.com (npm ci, trusted publishing), pip's secure-installs guide, and the SLSA v1.2 spec. Checked October 2026.
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 types and the defenses that hold up. For proving what shipped, see our companion post on supply-chain security with SBOMs and attestation.
How the attacks actually reach you
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. Hallucinated names tend to repeat 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 got code running inside dozens of large companies this way 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. Worms now automate this: the Tensorlake npm compromise went after the AI tool configs on developer machines.
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 2024 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, layered across install, build and publish
No single control covers this. You need layers.
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:
# 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
# requirements.lock (hashes make substitution detectable)
requests==2.34.2 \
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0
pip turns hash-checking on for the whole file as soon as any line carries a --hash, and then every dependency must be pinned with == and hashed. --require-hashes forces the mode even when nobody remembered to add hashes, which is why it belongs in deploy scripts. Do the same for infrastructure code: a Terraform lock file is the provider-side equivalent.
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:
# .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. On npm 11 and earlier, ignore-scripts defaults to false, so set it explicitly. npm 12 changes the default posture: dependency install scripts that no one has reviewed are blocked with a warning, and the policy lives in an allowScripts field in package.json or .npmrc; set strict-allow-scripts to turn the warning into a hard failure. Either way, allowlist the few packages that genuinely need lifecycle hooks, and 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. npm's trusted publishing does exactly this for package releases from GitHub Actions, GitLab CI/CD and CircleCI cloud runners (npm CLI 11.5.1 or later; self-hosted runners aren't supported yet). 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 SLSA build provenance so you can answer "what is in this, and where did it come from" during an incident.
The order we'd do it in
Sequence the work by payoff per hour:
- Lockfiles with hashes plus install scripts blocked 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 and trusted publishing; delete long-lived publish secrets.
- Artifact signing plus verification, then SBOM and provenance for response.
Questions people ask
What is a software supply chain attack?
It is an attack on something your software depends on rather than on your code directly: an open-source package, a maintainer's registry account, a base container image, a build plugin, or the CI system that publishes your artifacts. Because every downstream consumer trusts that component, one compromise spreads to all of them. Typosquatting, dependency confusion, malicious install scripts, and stolen CI tokens are the common entry points.
How do you prevent dependency confusion?
Make sure internal package names can only resolve from your private registry. In npm, reserve an organization scope such as @acme on the public registry and map that scope to your internal registry in .npmrc. In Python, point installs at one index that proxies the public one instead of mixing --extra-index-url sources. Then install from a hash-pinned lockfile so a higher-versioned public impostor cannot substitute silently.
Does npm ci --ignore-scripts break anything?
Sometimes. Packages that compile native code or download binaries in postinstall (image processing, database drivers, some CLIs) will install but not work. The fix is an allowlist, not dropping the flag: on npm 12, approve those packages in the allowScripts policy; on older npm, keep --ignore-scripts and run npm rebuild <package> for the few that need it. Explicitly invoked scripts such as npm test still run.
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, blocked 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.
Sources
Cite this article
Kiril Urbonas (2026). Software Supply Chain Attacks: How They Work & Defenses. DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/software-supply-chain-attacks
[Software Supply Chain Attacks: How They Work & Defenses](https://www.devopsness.com/blog/software-supply-chain-attacks) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)
More from DevOps
Explore more articles in this category
Sign Container Images With Cosign: Keyless GitHub Actions Workflow, SBOM and In-Cluster Checks
One workflow that builds, signs keylessly with GitHub OIDC, attaches an SBOM and SLSA provenance, plus the Kyverno policy that refuses anything unsigned. Copy it, change three names, ship it.
GitLab CVE-2026-110264: A Second CVSS 10 RCE in Nine Days. Patch to 19.5.1
An unauthenticated regex-compiler RCE reaching back to GitLab 7.0, no workaround, and backports on older branches that fix only the critical bug. The exact version per branch and the commands to get there.
18 Kubernetes Interview Questions With Answers, by Level
Kubernetes interviews in 2026 are scenario-led. Eighteen questions grouped junior, mid and senior, each with the answer, the first command, and what separates a strong answer from a memorized one.
You might have missed
Evergreen posts worth revisiting.