OWASP Top 10:2025 Course — Module 3 of 10. Course home · Previous: Module 2 — Security Misconfiguration · Next: Module 4 — Cryptographic Failures

Learn first: what is a Software Supply Chain Failure

A03:2025 covers trusting third-party code without verifying what it actually does. The textbook attack: a dependency (or a dependency of a dependency) runs a script automatically when it’s installed — npm‘s postinstall hook — and that script quietly reads environment variables or files and sends them somewhere. npm install still prints 0 vulnerabilities, because vulnerability scanners only know about published CVEs, not a brand-new malicious package behaving exactly as its package.json told it to.

npm install
   │
   ├─▶ installs shopcart-api's real deps
   └─▶ installs "malicious-logger"
            │
            └─▶ runs postinstall: install-hook.js
                     │
                     ├─ reads process.env.ROGUE_DEMO_API_KEY
                     ├─ writes it to .loot/exfil-*.json
                     └─ POSTs it to an attacker server

This mirrors real incidents like the 2025 "Shai-Hulud" npm worm, which harvested secrets at install time the same way.

Scenario briefing

You maintain shopcart-api, a small internal service. A teammate added a new logging dependency last sprint to "clean up our console output." You’re doing a routine supply-chain audit before the next release.

Lab setup

  1. Download: Download the lab (zip)
  2. Prerequisites: Docker Engine 20+, Docker Compose v2 (no local Node.js install needed — it’s all in the container).
  3. Start the attacker’s collection endpoint:
    cd owasp-a03-supply-chain-lab
    docker compose up -d attacker
  4. Check it worked: docker compose logs attacker should show listening on http://0.0.0.0:4444/collect.

Missions

Mission 1 — trigger the attack. Install the victim project and watch what happens.

Show solution

Solution — Mission 1

docker compose run --rm victim

npm install reports 0 vulnerabilities — completely normal-looking. But the attacker container’s log shows a POST arriving with the harvested secret:

docker compose logs attacker
[attacker-server] received exfiltrated data:
{"harvestedAt":"...","variable":"ROGUE_DEMO_API_KEY","value":"FLAG{a03-postinstall-exfiltrated-the-key}"}

Mission 2 — generate an SBOM. Spot the dependency that resolves from a local path instead of the npm registry.

Show solution

Solution — Mission 2

docker compose run --rm victim sh -c "cd app && npm sbom --sbom-format cyclonedx | grep -B2 -A6 malicious-logger"

Look for "cdx:npm:package:path" pointing at ../malicious-logger — a production dependency resolved from a local filesystem path, not a published registry version, is a red flag a dependency-review step should catch automatically.

Mission 3 — find the hook. Locate the install-time script inside node_modules and read what it does.

Show solution

Solution — Mission 3

docker compose run --rm victim sh -c "cd app && grep -r postinstall node_modules/*/package.json && cat node_modules/malicious-logger/install-hook.js"

package.json declares "postinstall": "node install-hook.js". The script reads ROGUE_DEMO_API_KEY, writes a .loot/ file, and POSTs it to 127.0.0.1:4444. It contains FLAG{a03-found-the-postinstall-hook} right above the exfiltration code — read it carefully, that’s the actual malicious logic hiding in a package that advertises itself as "a tiny console logging helper."

Mission 4 — prevent it. Stop install scripts from running for untrusted packages, then verify.

Show solution

Solution — Mission 4

echo "ignore-scripts=true" > workspace/app/.npmrc
python3 lab.py verify

With ignore-scripts=true, npm install still installs malicious-logger as a declared dependency (it’s a real file: dependency the app needs to run), but the postinstall hook never executes — no .loot/ file, no network call, and the app still starts normally. In CI, use npm ci --ignore-scripts and allowlist only the handful of packages that legitimately need a native build step.

Hints

Fix it + re-test

The real fix is process, not code: disable arbitrary script execution at install time by default (ignore-scripts=true or npm ci --ignore-scripts), review any package that needs scripts re-enabled, and check SBOMs for dependencies resolved from local paths or unexpected registries in CI, not just at release time.

Detect it

Generate and diff SBOMs on every dependency change (npm sbom), flag any new postinstall/preinstall script in a PR, and monitor outbound connections from your build/CI environment — a build box making a request to an unfamiliar host during npm install is exactly what this attack looks like on the wire.

Quick self-check

  1. Why did npm audit report zero vulnerabilities even though the package is actively malicious?
  2. What’s the practical difference between "reviewed this package’s code" and "it has a lot of GitHub stars"?
  3. Why does ignore-scripts=true not break installing malicious-logger as a dependency?
Show answers

Answers

  1. npm audit only matches against a database of known, disclosed vulnerabilities (CVEs) — a brand-new or private malicious package has no CVE, so there’s nothing to match against.
  2. Popularity is a social signal, not a technical one — it tells you how many people installed it, not what the install script does; only reading the code (or at minimum the scripts block in package.json) tells you that.
  3. ignore-scripts only suppresses the preinstall/install/postinstall script execution step; the package’s files are still copied into node_modules and its exported functions (like log()) still work normally.