ML Supply Chain Security advanced 8 min read 6 flashcards

Containing Install and Build Time Execution

Why arbitrary code runs before any of your tests do, what disabling lifecycle scripts and constraining egress actually buys, and how to keep dependency resolution and dependency execution as separate, separately-governed events.

The most consequential code execution in a software project happens before a single line of that project runs. npm install and pip install execute publisher-supplied scripts, setup.py runs at build, and both operate with the invoking user's full environment: their shell, their dotfiles, their cloud credentials, their network. Every large ecosystem incident of 2025, s1ngularity and both Shai-Hulud waves included, worked through this channel. The malware never had to be imported by the application.

This is the layer where containment is cheapest, because nothing legitimate about resolving a dependency requires running its author's code on your laptop.

Separate resolution from execution

Resolution is a lookup: read a manifest, pick versions, fetch artefacts, verify hashes. It is pure and it is safe. Execution is everything else. Treating them as one command is a historical accident, and the ecosystems have started to unwind it. npm's post-Shai-Hulud hardening included disabling classic token creation in November 2025 and revoking remaining classic tokens on 9 December 2025, pushing publishing toward OIDC trusted publishing from CI (GitHub, 2025). Semgrep's analysis of npm v12 reports the more direct change: dependency lifecycle scripts no longer run by default (Semgrep, 2026, RIP npm postinstall scripts).

For anything not yet on those defaults, the controls that matter are ordinary and unglamorous:

  • Install with scripts disabled, and maintain a short explicit allowlist of packages that genuinely need a build step.
  • Resolve against an internal mirror that only serves versions already fetched and scanned, which also closes dependency confusion, the attack Alex Birsan used against more than 35 companies in February 2021 (Birsan, 2021, Dependency Confusion) and which struck PyTorch's nightly channel through torchtriton that December (PyTorch, 2022).
  • Run installs in a container with no access to the credential store and no outbound network except the registry.
  • Give CI runners short-lived, narrowly-scoped credentials, on the assumption that the install step is hostile.

Egress is the control that generalises

Signature-based detection fails against payloads that differ every run, and prompt-driven malware differs every run by construction. What does not vary is the need to get data out. A build environment whose outbound network is restricted to the package registry and the artefact store turns credential theft into a local, useless read; s1ngularity exfiltrated to GitHub, which is reachable from almost every CI environment on earth precisely because everyone allows it.

Egress control is also the only layer that treats hallucinated packages, tool poisoning and worms identically, because it does not care how the code arrived.

The trust asymmetry worth internalising

Test suites run after install. Scanners run on the resolved tree. Reviews happen on pull requests. All three are downstream of the moment a stranger's code ran as you. Any defence positioned after install is describing a machine that is already compromised, which is why detection budgets spent on post-install scanning buy less than a sandbox that costs one flag.

When it breaks

Allowlists for build steps become permanent. The packages that need compilation are exactly the low-level ones with the widest reach. An exception list that starts at four entries and is never revisited is the attack surface again, minus the alarm.

Lockfiles are not a substitute. A lockfile pins what will be installed; it says nothing about what that artefact does during installation. Pinning a compromised version reproducibly is not protection, and it is why lockfile-only policies survived none of 2025's incidents.

Container isolation with the socket mounted is not isolation. Installing inside a container that has the Docker socket, the host home directory, or a long-lived cloud role attached reproduces the original exposure with more steps. The boundary is only as good as what crosses it.

Developer machines are the weak side. CI is easier to constrain than a laptop, and both incidents began on laptops. Policy that applies only to CI leaves the machines with SSH keys and signed-in AI tools running installs unconstrained.

Registry hardening protects the ecosystem, not you. Trusted publishing, mandatory phishing-resistant 2FA and attestations raise the cost of hijacking a legitimate package. They do nothing about a malicious package published legitimately under an invented name, which is exactly the slopsquatting case. The two controls are complementary and neither substitutes for the other.

References and further reading

Every source this page cites, in the order it cites them. All of them open in a new tab.

  1. GitHub, 2025 github.blog
  2. Semgrep, 2026, RIP npm postinstall scripts semgrep.dev
  3. Birsan, 2021, Dependency Confusion medium.com
  4. PyTorch, 2022 pytorch.org
Check yourself

6 flashcards for this concept

Click a card to reveal the answer.

Drill the whole track