# Supply Chain Attack

> A supply chain attack compromises a trusted supplier, like a package, build pipeline, or update server, so its own channel delivers malicious code to users.

Also known as: software supply chain attack, supply-chain attack, supply chain compromise, open source supply chain attack, T1195

Source: https://socket.dev/glossary/supply-chain-attack

Last updated: 2026-10-26

A supply chain attack, also called a software supply chain attack or supply chain compromise, is an attack on a trusted supplier, such as an open source package, a build pipeline, or an update server, that turns the supplier's own distribution channel into the delivery mechanism. Victims install the malicious code themselves, through a channel they already trust.

The mechanism is delegated trust. A package manager, an auto-updater, or a CI runner fetches whatever a supplier publishes and runs it with the consumer's own privileges, so the attacker never has to break into the victim. One malicious release reaches every consumer whose next install or update resolves to it. In 2025 and 2026, most malicious npm releases ran their payloads during installation, before any application code loaded and before any advisory existed.

The fact that matters most is speed. The [xz-utils backdoor](https://socket.dev/blog/how-to-use-socket-to-find-out-if-you-were-affected-by-the-backdoored-xz-package) sat in release tarballs for 34 days before anyone reported it. The [malicious axios release](https://socket.dev/blog/axios-npm-package-compromised) in March 2026 was publicly reported in two and a half hours, and that was still long enough. Our position: a supply chain attack has to be stopped where code is fetched and executed, by analyzing what the artifact actually does. CVE scanning cannot do that, because malware rarely gets a CVE. Signatures and provenance cannot do it alone, because the 2026 worms published through their victims' own legitimate pipelines.

## What is a supply chain attack?

A supply chain attack reaches its victims through something they obtained from a supplier, not through their own systems. MITRE ATT&CK catalogs it as technique [T1195, Supply Chain Compromise](https://attack.mitre.org/techniques/T1195/), and defines it in one sentence: "Adversaries may manipulate products or product delivery mechanisms prior to receipt by a final consumer for the purpose of data or system compromise."

MITRE splits the technique by supplier type: software dependencies and development tools (T1195.001), software products and their update channels (T1195.002), and hardware (T1195.003). This entry covers the first two, because that is where the volume is. A hardware implant needs physical access to a factory or a shipment. A software implant needs one publish credential, and every link of the [software supply chain](https://socket.dev/glossary/software-supply-chain) holds one.

Two boundaries are worth drawing. A vulnerable dependency, such as Log4Shell in 2021, is a supply chain risk but not a supply chain attack: nobody planted the bug. A new [malicious package](https://socket.dev/glossary/malicious-package) published under a lookalike name, such as a [typosquat](https://socket.dev/glossary/typosquatting), attacks the consumer's choice of supplier rather than a supplier they already trust. We count both typosquats and hijacked releases as supply chain attacks, because in both cases the registry delivers the payload. Hijacked releases do more damage, because version ranges install them for you.

## How does a supply chain attack work?

A supply chain attack works by taking control of one link that consumers have already authorized to send them code, then waiting for their tools to fetch and run it. That authorization is built into ordinary dependency management, for good reasons.

Modern tooling delegates three decisions to suppliers. The first is which code you get: a version range, an auto-updater, or a floating container tag lets the supplier choose the next version. The second is when it runs: npm lifecycle scripts, Python startup hooks, Cargo build scripts, and CI steps execute supplier code during install, startup, or build. The third is with what privileges: the installing user's, which on a CI runner include deploy keys and publish tokens. Each delegation exists to save work. Ranges deliver bug fixes without a manual upgrade, install scripts compile native modules, and CI needs credentials to ship.

A single line of a manifest shows the first delegation:

```json
{
  "dependencies": {
    "axios": "^1.14.0"
  }
}
```

On March 31, 2026, that range resolved to the malicious axios@1.14.1 on any fresh install. Nothing in the consumer's repository changed. The new version added one dependency, plain-crypto-js@4.2.1, whose postinstall script dropped a remote access trojan.

Every supply chain attack answers three questions in order, and each answer is a place where a defense can act:

- **Entry:** which trusted link the attacker controls. Usually a maintainer account, a maintainer's identity, a build system, or a hosted script or CI action.
- **Reach:** how the payload travels from that link to victims. Range resolution, signed auto-updates, mutable tags and floating images, or self-propagation.
- **Trigger:** when the victim's own tooling executes it. An install hook, interpreter startup, a build or CI step, or the application's runtime.

> Figure 1. Every supply chain attack combines an entry point, a reach mechanism, and a trigger. Examples are the incidents in the table below.
>
> The three stages of a supply chain attack: entry, reach, and trigger, with example incidents for each

Figure 1 places the three stages along the path code travels, from supplier to distribution to consumer. Entry happens on the supplier side: a maintainer account (the [Qix compromise](https://socket.dev/blog/npm-author-qix-compromised-in-major-supply-chain-attack), axios, [ua-parser-js](https://socket.dev/blog/limitations-of-cve-based-security-scanners)), a maintainer's identity (the [event-stream](https://socket.dev/blog/limitations-of-cve-based-security-scanners) handoff, the xz-utils co-maintainer), a build or CI system (SolarWinds, the [TanStack compromise](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack), the [LiteLLM compromise](https://socket.dev/blog/litellm-compromise)), or a hosted script or action (Codecov, the [tj-actions/changed-files compromise](https://socket.dev/blog/github-actions-supply-chain-attack-puts-thousands-of-projects-at-risk)). Reach happens in distribution: a version range resolving to the new release (^1.14.0 to axios 1.14.1), a signed auto-update (SolarWinds Orion, fewer than 18,000 customers), a mutable tag or floating image (tj-actions v1 through v45.0.7, LiteLLM's latest tag), or self-propagation (the [Shai-Hulud worm](https://socket.dev/blog/happy-birthday-shai-hulud), [Mini Shai-Hulud](https://socket.dev/supply-chain-attacks/mini-shai-hulud), and [Miasma](https://socket.dev/supply-chain-attacks/miasma-mini-shai-hulud-supply-chain-attack)). The trigger happens on the consumer side: an install hook (preinstall, postinstall, binding.gyp, a git dependency's prepare script), interpreter startup (LiteLLM's .pth and sitecustomize.py files), a build or CI step (the xz tarball's m4 file, Codecov, tj-actions), or runtime in production (SUNBURST, the Qix wallet hooks in browsers). Supplier controls act on entry, registry and resolver controls act on reach, and consumer controls act on the trigger.

## Supply chain attacks from event-stream to LiteLLM

The twelve incidents below cover each entry point, reach mechanism, and trigger in Figure 1. Dates are the first malicious release.

| Date | Attack | Entry | Reach | Trigger |
| --- | --- | --- | --- | --- |
| Sep 2018 | event-stream | Maintainer handoff: a volunteer got publish rights, then added the flatmap-stream dependency | Semver range on a transitive dependency | Build time, only inside the Copay wallet's release build |
| Mar 2020 | SolarWinds SUNBURST | Covert access to the Orion build process | Signed vendor updates installed by fewer than 18,000 customers | Runtime, inside the updated Orion product |
| Jan 2021 | Codecov Bash Uploader | Credential extracted from a Docker image, used to alter the script | CI pipelines downloading the script on every run | Each CI run, which sent environment variables to the attacker |
| Oct 2021 | ua-parser-js | Hijacked npm account | Semver ranges on three release lines | preinstall.js, dropping a Monero miner and a password stealer |
| Feb 2024 | xz-utils | A co-maintainer with commit and release rights | Linux distribution packages built from release tarballs | Build, through m4 files absent from Git, then runtime in sshd |
| Mar 2025 | tj-actions/changed-files | A likely stolen credential, used to repoint tags v1 through v45.0.7 | Mutable tags referenced by more than 23,000 repositories | Inside CI, printing secrets to workflow logs |
| Sep 2025 | chalk, debug, and 17 more | Phishing email for a fake npm 2FA reset | Packages with 2 to 3 billion weekly downloads | Runtime, in end users' browsers, swapping crypto wallet addresses |
| Sep 2025 | Shai-Hulud | Stolen npm tokens | Self-propagation into each victim maintainer's other packages | postinstall, running TruffleHog to harvest secrets |
| Mar 2026 | axios | Hijacked maintainer account, publishing outside the normal release workflow | Caret ranges on a package with 100 million weekly downloads | postinstall in the new plain-crypto-js dependency |
| May 2026 | Mini Shai-Hulud via TanStack | CI: a pull\_request\_target workflow, cache poisoning, and an OIDC token read from runner memory | Trusted publishing, with valid Sigstore provenance | A git dependency's prepare script |
| Jun 2026 | Miasma "Phantom Gyp" | Compromised maintainer credentials | Worm across npm, PyPI, GitHub Actions, and Go source | npm's implicit node-gyp rebuild of a binding.gyp file |
| Oct 2026 | LiteLLM 1.107.0 | A leaked key, then a stolen GitHub org token | PyPI wheels and floating container tags | Python interpreter startup through a .pth file |

Sources: [npm on event-stream](https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident), [CISA ED 21-01](https://www.cisa.gov/emergency-directive-21-01), [SolarWinds' SEC filing](https://www.sec.gov/Archives/edgar/data/1739942/000162828020017451/swi-20201214.htm), [Codecov](https://about.codecov.io/security-update/), [CERT-EU on ua-parser-js](https://www.cert.europa.eu/publications/security-advisories/2021-057/), and Socket's reports linked below.

Three patterns run down the columns.

The entry column is mostly people and credentials, not code bugs. The event-stream and xz-utils attackers earned maintainer trust. The Qix compromise started with a 2FA-reset email from support@npmjs.help. The axios, ua-parser-js, and Shai-Hulud cases were [maintainer account takeovers](https://socket.dev/glossary/maintainer-account-takeover). The exceptions are CI misconfigurations, such as Codecov's Docker image and TanStack's pull\_request\_target workflow, and those exposed credentials too.

The trigger column moved to install time. Five of the six npm incidents in the table since 2021 executed during npm install, through [install scripts](https://socket.dev/glossary/postinstall-scripts) or the implicit node-gyp build. Install time is where most supply chain attacks execute, and default-on dependency lifecycle scripts were a design mistake. [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) corrected it on January 7, 2025, and [npm 12](https://socket.dev/blog/npm-12) on July 8, 2026, according to the npm registry's publish times for both releases. Neither fix reaches the other triggers: the Qix payload ran in browsers, LiteLLM ran at Python startup, and tj-actions ran inside CI.

The reach column needs no mistake from the victim. Every row used a mechanism working as designed: a caret range, a signed update, a tag, a [transitive dependency](https://socket.dev/glossary/transitive-dependency) nobody chose directly. That is the defining property of a supply chain attack, and the reason "be careful what you install" is not a defense.

## Supply chain attacks went from months to hours, but only the loud ones

Supply chain attacks that once stayed hidden for months are now reported within hours, as long as they are loud. Figure 2 measures each incident from the first malicious release to the first report we can date from a primary source.

> Figure 2. Time from the first malicious release to the first report we can date from a primary source. Endpoints for each bar are listed in the table below.
>
> Log-scale bar chart of how long nine supply chain attacks were live before the first report, from about 9 months for SolarWinds to about 2 hours for LiteLLM

| Attack | First malicious release | First dated report | Window |
| --- | --- | --- | --- |
| event-stream | event-stream@3.3.6, Sep 9, 2018 08:28 UTC (npm registry) | npm security team notified, Nov 26, 2018 | 78 days |
| SolarWinds SUNBURST | Orion updates from March 2020 (SEC 8-K) | CISA Emergency Directive 21-01, Dec 13, 2020 | About 9 months |
| Codecov Bash Uploader | Jan 31, 2021 (Codecov) | Customer report, Apr 1, 2021 | 60 days |
| ua-parser-js | 0.7.29, Oct 22, 2021 12:15 UTC (npm registry) | Clean 0.7.30 published 16:16 UTC | 4 hours |
| xz-utils | v5.6.0 tagged Feb 24, 2024 | Public disclosure, Mar 29, 2024 | 34 days |
| chalk and debug | debug@4.4.2, Sep 8, 2025 13:12 UTC (npm registry) | Socket report, 15:16 UTC | 2.1 hours |
| Shai-Hulud | @ctrl/tinycolor@4.1.1, Sep 15, 2025 19:52 UTC (npm registry) | Socket report, 22:50 UTC | 3.0 hours |
| axios | axios@1.14.1, Mar 31, 2026 00:21 UTC (npm registry) | Socket report, 02:52 UTC | 2.5 hours |
| LiteLLM 1.107.0 | PyPI wheels, Oct 21, 2026 11:39 UTC | Socket report, 13:41 UTC | 2.0 hours |

For the npm and PyPI rows, the endpoint is Socket's published report, so each window is an upper bound. Automated alerts came sooner. Socket flagged plain-crypto-js@4.2.1 six minutes after it was published, before axios@1.14.1 existed, and flagged the LiteLLM wheels seven minutes after publication.

Read the chart as two populations, not a trend line. The long windows belong to quiet, targeted payloads. event-stream activated only inside one wallet's release build, and the xz backdoor only inside sshd on systems that linked it. The short windows belong to loud ones. A worm or a credential stealer that wants reach has to publish widely and fast, and fast publishing is exactly what publish-time scanners watch. ua-parser-js sits with the fast group even in 2021, and it dropped a cryptominer, which is about as loud as malware gets. The lesson is not that detection got better at everything. We see no evidence that a patient, targeted attacker is caught faster in 2026 than in 2024.

Short windows also do not mean small exposure. An install-time payload executes on the first fresh install, and CI fleets install continuously. A few hours of caret ranges resolving to axios@1.14.1 was enough to reach CI pipelines through npx and transitive ranges, as our analysis of [the hidden blast radius of the axios compromise](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise) showed.

The npm timestamps above are reproducible. The registry keeps a version's publish time in the package document's time field even after the version itself is removed:

```sh
for spec in event-stream@3.3.6 ua-parser-js@0.7.29 chalk@5.6.1 \
            @ctrl/tinycolor@4.1.1 axios@1.14.1; do
  pkg=${spec%@*}; ver=${spec##*@}
  curl -s "https://registry.npmjs.org/${pkg/\//%2f}" |
    jq -r --arg p "$pkg" --arg v "$ver" \
      '[$p, $v, .time[$v], (.versions | has($v))] | @tsv'
done
```

Output on October 26, 2026. The last column says whether the registry still serves the version:

```text
event-stream	3.3.6	2018-09-09T08:28:59.503Z	false
ua-parser-js	0.7.29	2021-10-22T12:15:21.378Z	false
chalk	5.6.1	2025-09-08T13:13:05.239Z	false
@ctrl/tinycolor	4.1.1	2025-09-15T19:52:46.624Z	false
axios	1.14.1	2026-03-31T00:21:58.168Z	false
```

The record has gaps. When npm replaces a whole package with a security placeholder, its history goes too: flatmap-stream, which carried the event-stream payload, now lists only two timestamps, from November 28 and 29, 2018, and none for the release that carried the payload. PyPI keeps less. On the same day, the JSON API for litellm 1.107.0 returned HTTP 404, and the release no longer appears in the project's version list. If you are reconstructing your own exposure after an incident, capture registry metadata while it still exists.

## Why are software supply chain attacks increasing?

Software supply chain attacks are increasing because each successful one now funds the next. Four changes made that true.

**One publish is a broadcast.** The 19 packages hijacked in the Qix compromise receive 2 to 3 billion downloads a week combined. axios receives about 100 million. Few initial-access techniques give an attacker that many execution points for one stolen credential.

**Stolen credentials compound.** Install-time payloads run on exactly the machines that hold publishing credentials: developer laptops and CI runners. The Shai-Hulud worm of September 2025, the first known self-propagating npm worm, used the npm tokens it stole to republish itself into its victims' other packages. When Australian police arrested two alleged members of [TeamPCP](https://socket.dev/blog/teampcp-targeting-security-tools-across-oss-ecosystem) in August 2026, the Australian Federal Police [estimated](https://socket.dev/blog/happy-birthday-shai-hulud) the group had enabled the theft of more than 500,000 credentials across more than a thousand organizations.

**The tooling is public.** In May 2026, TeamPCP released the worm's source code and [co-launched a $1,000 BreachForums contest](https://socket.dev/blog/teampcp-supply-chain-attack-contest) scored by the download counts of compromised packages. By mid-2026, multiple operators were running Shai-Hulud-style campaigns from the same code. A technique that once required a capable team now requires a forum account.

**The attack surface moved into CI.** [Trusted publishing](https://socket.dev/blog/npm-trusted-publishing) replaced long-lived npm tokens with short-lived OIDC credentials minted inside CI. Attackers followed the credential. The Mini Shai-Hulud wave that hit TanStack in May 2026 stole no long-lived npm token: attacker code ran inside TanStack's GitHub Actions workflow, read an OIDC token from runner memory, and published with valid Sigstore provenance. As Socket's TanStack analysis put it, "an attacker who can execute in GitHub Actions can generate valid Sigstore attestations for malicious packages."

AI agents add a fifth pressure. Coding agents install packages without a human reading the name. In September 2026, the UK AI Security Institute [reported that GPT-6 Astra](https://socket.dev/blog/astra-supply-chain-attacks) reached the payload-delivery stage of a supply chain attack in 29.2% of simulated capture-the-flag runs with OpenAI's cyber classifiers disabled. It had not been asked to attack open source.

We do not quote a headline count of attacks per year. Published tallies count packages, versions, or incidents, and are not comparable with each other. The defensible measure of the trend is the one above: the cost of an attack has fallen while its yield has risen. The OWASP Top 10 2025 reflects the same shift: it added [Software Supply Chain Failures](https://socket.dev/blog/owasp-2025-top-10-adds-software-supply-chain-failures) as a new category, ranked third.

## How do you prevent a supply chain attack?

You prevent a supply chain attack by breaking it at each of its three stages separately, because no single control touches all three. Every control below stops some rows of the incident table and misses others.

| Control | Stage | Likely stops | Misses | Cost |
| --- | --- | --- | --- | --- |
| Phishing-resistant 2FA, no long-lived publish tokens | Entry | Credential phishing like Qix; stolen tokens like Shai-Hulud | Code running inside a trusted CI workflow (TanStack); leaked org keys (LiteLLM) | Migrating release automation |
| Least-privilege CI, no pull\_request\_target on untrusted code | Entry | The TanStack chain | A compromised maintainer with legitimate access | Workflow rewrites |
| Provenance policy pinned to the expected source repository | Entry | LiteLLM, whose provenance recorded Berri-AI/litellm | Malicious code built by the real pipeline (TanStack) | Policy upkeep per package |
| Lockfile and npm ci | Reach | Existing installs during the axios window | New dependencies, upgrades, npx, global installs | Low |
| Release-age delay of 24 hours or more | Reach | chalk, Shai-Hulud, axios, LiteLLM, all reported within hours | Slow attacks: xz (34 days), event-stream, SolarWinds | Urgent fixes wait unless exempted |
| Pin CI actions to full commit SHAs | Reach | tj-actions tag repointing | A pinned commit that is itself malicious | Manual or automated bumps |
| No dependency install scripts by default | Trigger | ua-parser-js, Shai-Hulud, axios, TanStack's git prepare, Phantom Gyp | Runtime payloads (Qix), Python startup hooks (LiteLLM), CI actions | Native modules need explicit approval |
| CVE and advisory scanning | After the fact | None of these before execution | Malware, until someone writes an advisory | Low |
| Behavior analysis of each artifact at fetch time | Reach and trigger | Loud payloads within minutes of publish | Quiet, targeted code; packages already in a local cache | A proxy or wrapper in the install path |

Four of these deserve detail.

A [lockfile](https://socket.dev/glossary/lockfile) freezes what you already resolved. It does nothing for the next resolution, and checking one after an incident proves little. In the words of our axios follow-up, "Lockfiles make existing installs deterministic. They do not make new installs safe." Code fetched by npx or a global install never appears in a project lockfile at all.

A [dependency cooldown](https://socket.dev/glossary/dependency-cooldown) refuses versions younger than a threshold. [pnpm 11](https://socket.dev/blog/pnpm-11-adds-new-supply-chain-protection-defaults) defaults to 24 hours, npm added a [minimum release age setting](https://socket.dev/blog/npm-introduces-minimumreleaseage-and-bulk-oidc-configuration) in its 11.x line, and [Rust 1.100](https://socket.dev/blog/rust-cargo-cooldowns) is scheduled to add an opt-in Cargo setting on November 12, 2026. Every npm and PyPI attack in Figure 2 was reported or replaced within about four hours, long before a one-day delay would have let it install. A cooldown is useless against xz-style patience, and it delays security fixes unless you exempt them.

[Package provenance](https://socket.dev/glossary/package-provenance) proves which pipeline built an artifact, not that the pipeline was honest. Provenance and signatures are necessary but not sufficient. They still pay off when a policy checks them: the LiteLLM image's provenance recorded the lookalike repository Berri-AI/litellm, which a rule pinning the expected source would have rejected.

Behavior analysis reads what the artifact does instead of what its metadata claims. Socket Firewall applies it at fetch time: [sfw](https://docs.socket.dev/docs/socket-firewall-free) wraps npm, pnpm, yarn, pip, uv, or cargo, filters their registry requests, and blocks package artifacts Socket has confirmed as malware. Its free tier warns on, but does not block, versions flagged by AI and not yet confirmed by a human, and it never sees packages served from a local cache.

Our verdict: consumers should run npm 12 or pnpm 10 and later, install from a committed lockfile with npm ci, set a release-age delay of at least 24 hours, pin CI actions to SHAs, and inspect everything a lockfile does not cover at fetch time. Publishers should remove long-lived tokens, require a human 2FA approval for each release, as [npm's staged publishing](https://socket.dev/blog/npm-12) does, and treat CI secrets as production credentials. None of this stops an attacker who already owns the supplier's organization.

## The release is the last step of the attack

The LiteLLM compromise shows where the remaining gap is. The malicious release was caught in minutes. The exposure that produced it lasted months: a leaked key sat unrevoked from May 2026, and attacker activity is documented from October 10.

> Figure 3. The LiteLLM compromise, from LiteLLM's attack-chain investigation and Socket's two reports. The public release was the last step, not the first.
>
> Timeline of the LiteLLM compromise: months of undetected access inside the supplier, then seven minutes from publish to a registry-side alert

Figure 3 lays out the sequence from [LiteLLM's investigation and our follow-up](https://socket.dev/blog/litellm-attack-timeline). In May 2026, an Agent Platform master key was committed to a public repository and never revoked. On October 10 at 22:11 UTC, a push to a throwaway branch named ci-tmp-a351cc ran a workflow called cache.yml that posted six organization secrets, including GH\_TOKEN, to a webhook.site collector. On October 12 at 12:16 UTC, a six-job version targeted 14 more secrets across five GitHub Environments. Each branch was deleted the day it was created. On October 21 at 10:51 UTC, the malicious release build started; its provenance recorded the source Berri-AI/litellm, and LiteLLM's audit logs tie the release approval to the token stolen on October 10. The seven 1.107.0 wheels reached PyPI at 11:39:51 UTC. Socket's AI scanner raised malware alerts at 11:46:30 UTC, seven minutes later, and Socket published its analysis at 13:41 UTC.

Registry-side detection now runs in minutes. Inside LiteLLM's own organization, the attacker worked undetected for at least 11 days, using access that traced back to a key exposed since May, and chose when to cash in. A push that runs a workflow on a branch deleted minutes later looks like routine CI to most organizations.

The next gain in supply chain security will not come from scanning releases faster. It will come from suppliers treating their CI secrets, org tokens, and release approvals with the audit trails and anomaly detection they already apply to production, because for everyone downstream, that is what they are.
