A software supply chain attack compromises something used to build, distribute, or update software—not necessarily the final application itself. That can make the attack difficult to spot: a familiar update, dependency, or build artifact may carry malicious changes into otherwise trusted environments.
This article explains how these attacks work and what incidents involving SolarWinds, Codecov, npm’s ua-parser-js, and XZ Utils reveal. It also covers practical defenses: dependency pinning, provenance, signing, permission controls, and incident response.
Key takeaways
- A supply chain attack targets a trusted relationship or component, such as a dependency, build system, maintainer account, or update channel.
- A valid signature or a pinned dependency helps answer specific security questions, but neither proves that software is safe.
- Provenance helps establish where and how an artifact was built; restricted permissions limit what a compromised component can do.
- Incident response should account for both the affected software and credentials, systems, and downstream consumers it could have exposed.
What is a software supply chain attack?
A software supply chain includes the people, services, code, and processes involved in creating and delivering software. It can include source-code repositories, open-source packages, build runners, deployment credentials, signing systems, package registries, and automatic update mechanisms.
A software supply chain attack compromises one or more of these elements to affect software or its users. Instead of breaking directly into every target, an attacker may tamper with a trusted dependency or delivery process so that malicious code reaches downstream projects through a routine workflow.
The compromised point can vary. An attacker might take over a maintainer account, insert code into a dependency, steal a build credential, alter a build environment, or abuse an update channel. These methods have different evidence and consequences; “supply chain attack” describes a broad class of incidents, not a single technique.
How the compromise reaches users
A typical chain might look like this:
- An attacker gains access to a package maintainer’s account or a build system.
- They publish a modified package or alter the build output.
- A downstream project downloads or installs the affected version.
- The code executes during installation, application runtime, or another build step, depending on the package and workflow.
- The attacker attempts to access data or systems available to that code.
Not every incident follows these steps. For example, some attacks target the process that produces a release rather than changing the source repository in an obvious way. Understanding the specific path matters when deciding what to inspect and contain.
Lessons from real incidents
The examples below illustrate distinct points of compromise. The facts of an incident should be distinguished from the broader defensive lessons drawn from it; similar symptoms do not establish that two attacks used the same method.
SolarWinds Orion: trusted software updates
In 2020, attackers compromised the build process used for certain SolarWinds Orion software updates. The resulting updates contained malicious code and were distributed to customers through the vendor’s normal software update process. The incident demonstrated how a compromise upstream in a build-and-release pipeline can affect downstream organizations that trust the vendor’s distribution channel.
Lesson: Protect build and release systems as production infrastructure. Limit access to build credentials, separate build responsibilities where practical, monitor changes to release workflows, and preserve records that help determine which source and build process produced each artifact. A customer also needs a way to identify which versions were installed and where.
Codecov: a compromised upload script
In 2021, Codecov disclosed that an attacker had modified its Bash Uploader script. Organizations using the script could have run the altered version in their environments. Because upload scripts may run in automated builds, the incident raised concern about secrets and environment data accessible to those jobs.
Lesson: Treat third-party scripts executed in CI as code with the job’s permissions. Pin the script or action to a reviewed, immutable revision where possible; avoid fetching and executing a mutable script without verification; and keep sensitive credentials out of jobs that do not need them. Pinning reduces surprise from changes but does not make a malicious pinned revision safe.
ua-parser-js: a compromised package publishing account
In 2021, malicious versions of the popular npm package ua-parser-js were published after the package’s maintainer account was compromised. The affected releases were removed, but consumers that had installed them still needed to determine whether the versions entered their dependency trees and whether the package code ran in their environments.
Lesson: A package registry entry can be a distribution point, not a guarantee that every published version is benign. Use lockfiles and reproducible installation practices, monitor advisories, and keep an inventory of direct and transitive dependencies. When a package incident occurs, check the exact affected versions rather than assuming that every project using the package was affected—or that none was.
XZ Utils: malicious changes in release artifacts
In 2024, a backdoor was discovered in certain XZ Utils release tarballs. The discovery followed investigation of unusual system behavior. The affected releases were present in some pre-release or testing distributions; the incident was not equivalent to a compromise of every Linux system using XZ Utils.
Lesson: Reviewing a repository’s visible source alone may not explain every release artifact or build step. Compare release artifacts with their source, investigate unexpected build-time behavior, and make it possible to trace how an artifact was produced. The incident also showed the value of careful engineering investigation when performance or behavior changes do not have an obvious explanation.
Defenses that address different risks
No single control prevents every supply chain attack. A stronger approach combines controls that make unauthorized changes harder, easier to detect, and less damaging.
Pin dependencies—and understand what pinning does
A lockfile records resolved dependency versions, often including integrity data. In Node.js projects, npm ci installs from the lockfile and fails when the manifest and lockfile are out of sync. For Git-based dependencies or CI actions, pinning to a full commit hash is generally more stable than following a moving branch or tag.
For example, a project might use:
npm ci
The command does not assess whether a locked package is trustworthy. If a compromised version was already locked, repeatable installation can repeatedly install that same version. Pinning is primarily about controlling changes and making installations more reproducible; it should be paired with review, vulnerability monitoring, and a process for updating dependencies.
Review automated dependency updates rather than treating them as automatically safe. Check the package, version, change history, and any relevant advisory. Remove dependencies that are no longer needed, since each direct or transitive dependency adds code that may require maintenance and review.
Establish provenance
Provenance is evidence about an artifact’s origin and production: for example, which source revision and build process produced it. Provenance can help an organization assess whether an artifact came from an expected workflow and whether it corresponds to reviewed source.
For provenance to be useful, the evidence must be protected and verified. A statement generated by a compromised build environment may faithfully describe an untrusted build. Define which producers and workflows are trusted, retain evidence alongside artifacts, and verify it before deployment where your ecosystem supports it.
Sign artifacts and verify signatures
Digital signatures can help detect whether an artifact changed after it was signed and can identify the signing key used. Their value depends on the signing key and process: if an attacker can control the signer or steal its key, a malicious artifact may still receive a valid signature.
Protect signing keys, restrict who and what can invoke signing, and verify signatures as part of installation or deployment. Do not treat “signed” as a synonym for “safe”; it is one piece of evidence about integrity and origin.
Restrict permissions
Build jobs and dependencies should receive only the permissions they need. In CI, use short-lived credentials where available, limit token scopes, avoid exposing deployment secrets to untrusted pull-request code, and separate build, release, and deployment permissions when practical.
For GitHub Actions, a workflow can start with restrictive token permissions and grant additional access only to jobs that require it:
permissions: {}
jobs:
test:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<reviewed-full-commit-sha>
- run: npm ci
- run: npm test
Replace the placeholder with a reviewed full commit SHA before using the example. The required permission set depends on the workflow, and a restrictive token does not prevent every risk—for example, malicious code can still affect data accessible to the job or exploit other credentials that are exposed to it.
A practical response workflow
If a supplier, dependency, or build component is reported compromised, respond methodically rather than relying on a package name search alone.
- Confirm the advisory and affected scope. Identify the exact component, affected versions, time window, and known execution conditions using the vendor or project’s incident information and your organization’s approved sources.
- Find where it was used. Search manifests, lockfiles, build logs, artifact inventories, container images, and deployment records. Include transitive dependencies and cached artifacts where relevant.
- Contain exposure. Stop affected builds or deployments when warranted. Revoke or rotate credentials that may have been available to the compromised code, prioritizing tokens with broad access. Avoid simply rerunning a job with the same exposed secrets.
- Assess execution and impact. Determine whether the affected component was installed or executed, what data and permissions it could access, and whether there is evidence of unexpected activity. Installation alone does not prove exploitation, but it should not be dismissed without checking the package’s behavior and environment.
- Remove or replace affected artifacts. Upgrade to a version confirmed by the project or vendor as unaffected, or use a verified alternative. Rebuild from a known-good source and build process rather than assuming that deleting a dependency from the manifest removes it from deployed artifacts.
- Preserve evidence and communicate. Retain relevant logs and artifacts, follow internal incident procedures, and inform affected stakeholders as required. Record the versions, systems, and credentials assessed so the investigation can be reviewed.
- Fix the path that enabled exposure. After containment, reduce unnecessary permissions, improve dependency and artifact inventory, and update the review or release controls that failed.
These steps are a starting point, not a substitute for an organization’s incident-response plan. The right containment decision depends on the component, its privileges, and evidence available.
Common mistakes
- Assuming a lockfile guarantees safety. It makes dependency resolution more predictable; it does not establish that the selected code is benign.
- Trusting a signature without checking the signer. A valid signature is useful only when the signing identity and process are trustworthy.
- Treating provenance as proof of harmlessness. Provenance can help establish origin and build details, not certify that the source is secure.
- Giving CI broad credentials for convenience. A compromised dependency or action can inherit permissions available to its job.
- Checking only direct dependencies. A vulnerable or compromised package may enter through a transitive dependency or an artifact built elsewhere.
- Rotating a package version but not credentials. If malicious code could access secrets, replacing the package alone may leave an attacker with still-valid credentials.
- Assuming an advisory affects every user—or no users. Confirm exact versions, execution conditions, and deployment history.
Conclusion
A software supply chain attack works by turning trust in code, people, or delivery processes into an avenue for compromise. SolarWinds, Codecov, ua-parser-js, and XZ Utils show why defenses must cover more than dependency updates: teams need controlled builds, traceable artifacts, verified signatures, tightly scoped permissions, and a practiced response process.
Start with an inventory of what your software depends on and how it reaches production. Then make each step more reviewable and give each component only the access it needs. These controls cannot eliminate risk, but they can make compromises harder to introduce, easier to investigate, and less damaging.
