Supply-chain case study
Birsan research · Feb 2021 — PyTorch · Dec 2022In February 2021, one researcher walked into the internal networks of 35+ companies — Apple, Microsoft, PayPal, Shopify, Netflix, Tesla, Uber among them — without an exploit, a phish, or a stolen credential. He simply published packages named after their internal dependencies on public registries, with higher version numbers. The package managers did the rest. Twenty-two months later, the same attack hit PyTorch's nightly builds for real. This is the one attack class a properly configured registry ends outright.
What happened
Alex Birsan noticed that internal package names leak constantly —
in a stray package.json inside a public repo, in
build scripts, in error messages. Those names exist only on the
company's private feed. So he registered the same names on
public npm, PyPI, and RubyGems, with implausibly high version
numbers, and a harmless install-time callback that phoned home
the hostname, username, and install path over DNS.
The trap works because most resolver configurations treat a
package name as a single global identity. When both a private
feed and a public registry claim the same name, precedence
rules — often "highest version wins," or pip's
--extra-index-url, which offers no
priority guarantee at all — quietly decide which one you
install. Birsan's public impostors won inside build servers
and developer machines at more than 35 companies. The research
earned over $130,000 in bug bounties, and within days of
publication, public registries saw a wave of copycat uploads
probing other companies' internal names.
The proof that this was no one-off came at Christmas 2022.
PyTorch's Linux nightly builds depended on
torchtriton, which lived only on PyTorch's own
index. An attacker registered the name on public PyPI, and
between December 25 and 30 every pip install of
the nightly resolved the malicious PyPI copy instead. Its
payload exfiltrated /etc/passwd, SSH keys,
.gitconfig, environment variables, and the first
1,000 files of the user's home directory — over DNS. PyTorch
had to rename the dependency and instruct users to uninstall.
The root cause
No amount of developer vigilance fixes dependency confusion,
because the developer never sees the decision being made. The
lockfile says acme-internal-utils; the resolver
decides which acme-internal-utils. The
ambiguity lives in the resolution layer — which means the fix
has to live there too.
Microsoft's own post-incident guidance said exactly this: route every install through one feed that controls upstream precedence, rather than letting each client mix public and private indexes. That "one feed with explicit precedence" is precisely what a virtual repository is.
The OrbitalReg defence
Developers and CI resolve against one virtual repository with an explicit, ordered member list — local repos first, remote proxies last. If a name exists internally, the public upstream is never consulted for it. Resolution is deterministic; there is no "highest version wins" race for the resolver to lose.
Reserve your internal prefixes as policy. The naming-convention audit on the admin overview flags any artifact — local or proxied — that collides with a reserved namespace, so a public package shadowing an internal name surfaces as a finding instead of an install.
Remote-proxy repos fetch from configured upstreams only, and every artifact they admit is scanned and recorded in the append-only audit trail — which upstream served it, when, to whom. A malicious impostor doesn't slip in silently; it arrives as an auditable event you can alert on.
Dependency confusion requires a path from your build to a public registry. In air-gap mode there isn't one: artifacts enter through explicit, signed imports. The attack's precondition — ambient public-registry access — simply does not exist in the deployment.
For your records
torchtriton on public PyPI.
PyTorch-nightly installs on Linux resolve the malicious copy;
the payload exfiltrates SSH keys, /etc/passwd,
.gitconfig, and home-directory files via DNS.
pytorch-triton, reserves the name on PyPI, and
instructs affected users to uninstall and purge caches.
Honest caveats
The registry only protects installs that go through it.
A developer running pip install against public
PyPI directly, on a laptop with unrestricted egress, bypasses
every control above. The defence is complete only when client
configuration and network policy route package traffic through
the registry — which is an ops decision, not a product feature.
PyTorch's own users were hit through PyTorch's channel. The torchtriton victims installed from the vendor's index as instructed. A proxying registry would have helped only where it pinned or curated that upstream. If you configure arbitrary upstreams with open precedence, you have rebuilt the ambiguity this attack needs — inside your own registry.
Related reading. Name-based attacks have an AI-era successor: attackers registering package names that coding assistants hallucinate. We cover it in the slopsquatting case study, and the registry-worm variant in Shai-Hulud.
Primary sources
Worried about your internal package names?
Rico, the founder, walks through your current resolver configuration, where public registries can outrank your private feeds, and what an internal-first virtual-repo layout looks like for your formats.