Black Shard

Insights14 January 2026

Supply Chain Risk in Your Dependencies

A compromised npm or PyPI package runs on your CI runner with your deploy tokens long before it reaches production. Field notes on lockfile discipline, provenance, SBOMs and what a dependency review actually checks.

Stacked shipping containers receding into fog under a cold cyan floodlight at night

Your dependency tree is the attack surface

A modern Node or Python service ships far more code written by strangers than by your own team. Every package in the tree represents a maintainer account, a laptop and a publish pipeline you have implicitly trusted, and the trust is transitive: your direct dependencies chose their dependencies, who chose theirs. The part most teams miss is that compromise does not wait for production. npm runs dependency install scripts by default, and a Python source distribution executes its build code the moment pip builds it. The first machine to run a malicious package is almost never a production server. It is a developer laptop, or a CI runner holding deploy tokens, cloud credentials and access to every repository in the organisation.

That inversion decides where the controls belong. Teams review their own pull requests line by line, harden production networks, then run an unpinned install of thousands of unreviewed packages on the most privileged build machine they own. The registry boundary is a perimeter, and most organisations leave it wide open.

How a package becomes a payload

The routes in are well worn. Typosquatting plants near-name packages on npm and PyPI and waits for a typo, or increasingly for a plausible package name hallucinated by a coding assistant. Dependency confusion exploits internal package names that were never registered publicly: publish the same name to the public registry with a higher version number and a misconfigured resolver will prefer it, which is what researcher Alex Birsan demonstrated against major technology companies in 2021.

Account takeover is the productive route now. The event-stream incident in 2018 was a social handover: a burnt-out maintainer gave publish rights to a helpful volunteer, who shipped a payload targeting a specific bitcoin wallet. ua-parser-js in 2021 was a straight credential hijack, with malicious versions carrying a password stealer live on the registry for hours. In September 2025 a phishing email from a lookalike npm support domain took over the account behind some of the most downloaded packages on the registry, and the poisoned versions were pulled into installs across the ecosystem before the takedown landed later the same day.

Then there is the long game. The xz Utils backdoor discovered in 2024 followed years of patient contribution to a tired single-maintainer project until the attacker held the commit bit, then landed an obfuscated backdoor aimed at OpenSSH. It was caught by one engineer chasing a performance anomaly, not by any scanner. That is the common thread: these are attacks on trust and process. A vulnerability scanner comparing versions against CVE feeds sees nothing on day zero, because there is no CVE yet.

Lockfile discipline is the cheapest control you have

A lockfile records the exact resolved version and an integrity hash for every package in the tree, direct and transitive. It only protects you if it is enforced. Commit it. Build with npm ci, which installs exactly what the lockfile says and fails if it disagrees with the manifest, rather than npm install, which will quietly rewrite the lockfile to whatever resolves today. Treat lockfile diffs in pull requests as code, because they are: a one-line version bump can swap in an entirely different subtree.

The failure modes are consistent. A developer deletes the lockfile to fix a broken install and regenerates the whole tree. CI runs a different install command from the one developers use. The lockfile sits in the repository while the Docker build ignores it. Any of these turns your pinned tree back into a range resolution, and a caret range resolves to whatever exists at build time, including the version published twenty minutes ago by whoever now controls the maintainer account.

That is why release-age cooldowns work so well. Nearly every compromised release in the incidents above was identified and pulled within hours or days. A minimum release age of even a few days, now supported natively by pnpm and by update bots such as Renovate, means your builds simply never see the malicious window. Fast-track genuine security patches by exception; a cooldown on routine bumps and the ACSC Essential Eight expectation of prompt patching are compatible, because the urgency belongs to exploited vulnerabilities, not to every new minor version. And suppress install scripts where your tree tolerates it: pnpm now refuses to run dependency lifecycle scripts unless a package is explicitly allowlisted, and npm has supported ignore-scripts for years.

Provenance and SBOMs answer different questions

Provenance answers whether an artefact was really built from the source it claims, on the builder it claims. npm's provenance attestations, backed by Sigstore, tie a published package to a public commit and a CI run, and the SLSA framework gives the maturity ladder for how tamper-resistant that pipeline is. Provenance kills whole classes of attack, including packages published from a stolen laptop that match no public source. It does not help when the malicious commit is sitting in the real repository, as it was with xz Utils.

An SBOM answers a different question: what exactly is in the thing you shipped. Generate one per release in CI, in CycloneDX or SPDX, and store it with the artefact. Its value is not prevention, it is speed. When the next registry compromise makes the news, the difference between an organisation with SBOMs and one without is answering whether you are exposed, in which services and at which versions, in minutes from an inventory query instead of days of grepping images and lockfiles across repositories. Buyers, insurers and government procurement panels increasingly ask for one, which lets the same artefact do commercial work too.

What a review actually looks for

npm audit output is not a supply chain review. Much of what audit reports is unreachable advisories in dev-time tooling, and nothing this note describes would appear in it on day zero. A reviewer looking at dependency posture works through a different list: how many direct dependencies exist and what each is for, since every one drags in a subtree; which packages execute install scripts and whether that is suppressed; where single-maintainer packages sit in authentication, cryptography or payment paths; and whether internal package names are claimed or scoped on a private registry so dependency confusion is dead by construction.

Then the blast radius questions. If a postinstall script ran hostile code on your CI runner today, what could it reach: which tokens sit in the environment, how broadly they are scoped, and whether the build agent can make arbitrary outbound connections or only reach the registries it needs. Egress restriction on build infrastructure is one of the least fashionable and most effective controls in this whole area. This is the checklist Black Shard works through in secure code review engagements, because the dependency posture usually carries more risk than the first-party code around it.

Where to start this week

None of this needs a platform migration. The controls are configuration and habit, and most are a day's work across an estate. The checklist below covers them.

The regulatory framing is worth stating plainly. If personal information leaves through a compromised package, the Notifiable Data Breaches scheme under the Privacy Act 1988 makes that your eligible data breach, your OAIC notification and your customer conversation, regardless of the fact that the code arrived through a registry. You cannot outsource the accountability, so stop outsourcing the trust. Pin what you install, wait before you upgrade, know what you ship, and assume the next event-stream is already published.

  • Commit lockfiles everywhere and make CI install with npm ci or a frozen-lockfile equivalent, so any drift fails the build.
  • Put a minimum release age of a few days on automated dependency updates, with a fast-track exception for genuine security patches.
  • Disable dependency install scripts by default and allowlist the few packages that truly need them.
  • Claim or scope your internal package names on a private registry so a public lookalike can never win resolution.
  • Generate an SBOM for every release in CI and keep it with the artefact.
  • Scope CI tokens to the job at hand and restrict build-agent egress to the registries the build actually uses.

Know what your code would give away.

Australia-wide, from our Brisbane head office. Someone will contact you as soon as possible.

Open a briefinfo@blackshard.com.au