Software Supply Chain Attacks and How to Defend Against Them
Advanced4 minInnovialabs editorial team
In short
A supply chain attack targets not your code but what it is built from — packages, tools and CI; defense relies on pinning versions, blocking install scripts, SBOMs, signature verification and least privilege in CI.
The short answer
A supply chain attack is when an attacker does not break into your application directly but tampers with what it is built from: an npm or PyPI package, a GitHub Action, a Docker image, a build tool. The malicious code enters the project through an ordinary npm install and runs with the permissions of a developer or a CI job.
The core defenses:
- Pin exact versions and sources of dependencies.
- Do not run third-party install scripts unless needed.
- Know what is in your build — an SBOM.
- Verify signatures and the provenance of artifacts.
- Limit CI permissions so one compromised step does not unlock everything.
How the attacks work
Typosquatting. A package named almost like a popular one: a typo, an extra hyphen, swapped words. One wrong letter in npm install is enough.
Dependency confusion. If a company has an internal package such as billing-utils, an attacker publishes a package with the same name and a higher version to the public registry. A misconfigured package manager picks the public one.
Compromised maintainers. The attacker takes over a popular library author's account, or becomes a co-maintainer after slowly earning trust. Well-known examples include event-stream on npm, where a new maintainer added a dependency carrying malicious code, and the xz-utils backdoor discovered in 2024.
Malicious install scripts. The preinstall and postinstall fields in package.json run automatically on install. A script can read .env files, SSH keys and cloud tokens and send them out — before you have even started the app.
Tampering in CI. A third-party GitHub Action referenced by a tag like @v3 can be changed by pointing the tag at a different commit. Every pipeline using that tag will run the new code with access to its secrets.
Pinning versions and sources
- Commit the lockfile and install with
npm ci— it never changes versions. - Use scoped packages for internal libraries and bind the scope to your own registry explicitly:
# .npmrc
@company:registry=https://npm.company.internal/
- Do not upgrade to a version released an hour ago: malicious releases are often caught and pulled within days. Renovate has the
minimumReleaseAgesetting for this. - In CI, reference Actions by full commit SHA rather than by tag, and keep them updated with Dependabot or Renovate.
Install scripts
Most packages do not need install scripts. Disable them by default and allow them selectively for packages that truly require them (for example, those with native builds):
npm ci --ignore-scripts
Some package managers let you maintain an explicit allowlist of packages permitted to run scripts — use it.
SBOM
An SBOM (Software Bill of Materials) is a complete list of components in a build, with versions. It answers "are we affected?" when news breaks about a compromised package: instead of searching by hand, you query the SBOMs of all your services.
The standard formats are CycloneDX and SPDX. You can generate an SBOM like this:
npm sbom --sbom-format cyclonedx > sbom.json # in recent npm versions
syft dir:. -o cyclonedx-json > sbom.json # for any project or image
Generate an SBOM in CI for every build and store it next to the artifact.
Signature verification
npm audit signaturesverifies registry signatures and provenance attestations — proof of which repository and pipeline built a package.- When publishing your own packages, use
npm publish --provenancefrom CI. - For containers, use Sigstore cosign: sign images at build time and verify the signature before deploying.
cosign verify \
--certificate-identity-regexp "https://github.com/org/app/" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/org/app:1.4.0
Hardening CI
| Measure | What it gives you |
|---|---|
Minimal permissions for the CI token (contents: read by default) | A compromised step cannot write to the repository |
| Secrets only in the jobs and environments that need them | A leak is limited to one stage |
Care with pull_request_target and code from forks | Untrusted code never sees your secrets |
| Ephemeral runners | Malware does not persist between builds |
| Protected branches and required review | Nobody can quietly change the pipeline |
| OIDC instead of long-lived cloud keys | Nothing long-lived to steal |
For a systematic view of maturity, the SLSA framework and the OpenSSF Scorecard tool are useful.