OrbitalReg Sign in →

Supply-chain case study

Birsan research · Feb 2021 — PyTorch · Dec 2022

Dependency confusion: when the public registry outranks your own.

In 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

A name collision with code execution.

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

This is a registry-layer bug, not a developer error.

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

Four controls that make the ambiguity structurally impossible.

Virtual repos, internal-first

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.

Naming-convention audit

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.

Curated remote proxies

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.

Air-gap removes the precondition

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.

What this composes to. Birsan's attack worked because resolution was ambiguous and nobody was watching the tie-break. A virtual repo makes resolution deterministic; the namespace audit makes collisions visible; the audit trail makes every upstream fetch attributable. The attack doesn't get harder — it stops being expressible.

For your records

Dependency confusion timeline.

2021-02-09
Alex Birsan publishes "Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies." 35+ companies affected across npm, PyPI, and RubyGems; $130,000+ paid in bounties.
2021-02
Copycat squatting wave: within days, public registries see hundreds of probe packages named after other companies' internal dependencies. Microsoft publishes mitigation guidance centred on a single feed with controlled upstream precedence.
2022-12-25 → 30
An attacker registers 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.
2022-12-31
PyTorch discloses, renames the dependency to pytorch-triton, reserves the name on PyPI, and instructs affected users to uninstall and purge caches.

Honest caveats

Where the defence stops short.

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?

Map your namespace exposure in one call.

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.