
Between 25 and 30 December 2022, anyone who installed a nightly build of PyTorch on Linux with pip got a package called torchtriton that PyTorch never built. It came from PyPI, the public Python index, it was version 3.0.0, and the first time anything imported it, a binary inside read the machine's SSH keys, .gitconfig, environment variables and the first 1,000 files in the home folder, and sent them out as DNS queries.
Nothing of PyTorch's was broken into. No account, no build server, no signing key. PyTorch hosted torchtriton on its own index, download.pytorch.org; nobody had registered the name on PyPI, so a stranger did. pip was told to look at both indexes, saw 2.0.0 on one and 3.0.0 on the other, and picked the higher number. This is called dependency confusion, and any team that installs from a private index plus a public one can set itself up for it in one line of config.

The timeline
Download counts differ by who counted and when: 2,300+ after a week (BleepingComputer), 2,386 (Sophos), 2,717 (Snyk's tracking), about 2,800 (LWN). Each download is a machine or a CI job that pulled a nightly in the holiday week, mostly research and ML-engineering boxes with cloud keys and Git access.
Step 1: a package that lived only on a private index
PyTorch built its nightly Triton compiler as a separate wheel and served it from its own index. That index still lists the old files today, for example torchtriton-2.0.0+0d7e753227-cp310-cp310-linux_x86_64.whl: version 2.0.0 with a commit hash as a local label. The nightly torch wheel declared torchtriton as a dependency on Linux. Nobody owned the name on PyPI, which is the one condition this attack needs: a name your build depends on that you don't own on the public index.
Step 2: someone else registers the name
Anyone can create a PyPI project with an unused name in a minute, with no link to the private one. The uploader picked 3.0.0, higher than anything on PyTorch's index. The package's description said it was there to find dependency confusion bugs; a research probe sends back a hostname, not SSH keys.
Step 3: pip treats every index as one big list
The nightly install command looked like this:
--extra-index-url doesn't mean "look here if PyPI doesn't have it". pip collects candidates from all configured indexes and picks the best version that fits. There is no priority between indexes, and the order you list them in doesn't matter (pip issue #8606 is the long-standing request for one). We tested it with two local indexes and a dummy package, one index serving 2.0.0+0d7e753227, the other 3.0.0:
Two things stand out. With pip, swapping the order changes nothing: the higher number wins either way. With uv, the default first-index strategy takes a package only from the first index that has it, and uv searches --extra-index-url before --index-url. So PyTorch's exact command, run through uv, would have installed PyTorch's own 2.0.0; the same command with the two URLs swapped would not. uv protects you only if your private index is the one it asks first.
Step 4: a nightly install pulled it in
Only Linux + pip + nightly was hit. Stable PyTorch didn't depend on torchtriton, and conda installs resolved from PyTorch's own channel. Anyone who ran the nightly command that week, by hand or from a CI job that tests against nightly, got 3.0.0.
Step 5: import ran the binary
The package put a binary at site-packages/triton/runtime/triton (SHA-256 2385b29489cd9e35f92c072780f903ae2e517ed422eae67246ae50a5cc738a0e) that ran when triton was imported. PyTorch's own code didn't import it by default, so a machine that installed it but never used Triton may not have run it. What it collected, from PyTorch's advisory:
Step 6: out over DNS
The data left as encrypted DNS queries to subdomains of h4ck[.]cfd, through the resolver wheezy[.]io. DNS is allowed out of almost every network and CI runner, and an egress rule that only watches HTTP doesn't see it. The uploader later said the data had been deleted and the package was research; PyTorch's advice was to treat affected machines as compromised either way.
What breaks each step
Python: scope the sources
1. Stop using --extra-index-url for private packages with pip. Use one index (--index-url) that serves your packages and proxies PyPI (devpi, a Nexus or Artifactory virtual repo, AWS CodeArtifact with an upstream), so one place decides which torchtriton exists.
2. With uv, pin private packages to their index and mark it explicit, so it is used only for those packages and they come from nowhere else:
3. Lock with hashes. uv lock records each file's hash and source; with pip, pip-compile --generate-hashes, then pip install --require-hashes -r requirements.txt. A hash belongs to one file, so a 3.0.0 from somewhere else can't match it, and as our test shows, --require-hashes refuses anything not pinned with ==.
4. Claim your names on PyPI. A placeholder 0.0.1 under your organisation's account costs nothing; PyTorch registered pytorch-triton within five hours of hearing about it.
5. Run nightly or pre-release tests in their own CI job with no deploy keys or cloud credentials in its environment.
npm: scopes do this for you
npm ties a scope to a registry, so @acme/... packages only come from the registry you set for @acme. Unscoped internal names (acme-billing) go to the public registry if it has them.
• Create the organisation on npmjs.com too, so nobody else can publish under @acme/ there.
• Commit package-lock.json and install with npm ci: each entry records resolved (the exact URL) and integrity (a SHA-512), and npm ci fails if they don't match.
• Check for unscoped internal names: npm view <name> on the public registry should return 404 or a package you own.
If you installed a nightly in that window (or something like it)
1. pip3 uninstall -y torch torchvision torchaudio torchtriton and pip3 cache purge, then reinstall a nightly built after 30 December 2022.
2. Run PyTorch's one-line check for the binary (it looks for triton/runtime/triton in the environment).
3. Rotate what it read: SSH keys (remove the old public keys from GitHub, servers and authorized_keys), tokens and cloud keys that were in environment variables, anything in the first 1,000 files of the home folder.
4. Search DNS logs for h4ck.cfd to see which machines ran it.
Why this keeps working
Dependency confusion needs no breach and no malware skill, only a name your resolver will look up somewhere you don't control. The same pattern of a public index serving a stranger's code within hours shows up in the Ultralytics 2024 release (a real project's PyPI release poisoned through its CI cache) and the Shai-Hulud npm worm (stolen maintainer tokens). Pin what you install, by version and by hash. For install scripts and binaries you download by URL, the osec Pinned download check gives you the SHA-256 and a verify-before-run snippet.
Paste the URL of an install script or binary your build downloads and get its SHA-256 plus a download-verify-run snippet for shell, GitHub Actions, GitLab CI and Dockerfiles. Free.
Pin a downloadSources
• PyTorch: Compromised PyTorch-nightly dependency chain between December 25th and December 30th, 2022 (31 Dec 2022): https://pytorch.org/blog/compromised-nightly-dependency/
• BleepingComputer: PyTorch discloses malicious dependency chain compromise over holidays (2,300+ downloads, uploader's statement, h4ck.cfd registration): https://www.bleepingcomputer.com/news/security/pytorch-discloses-malicious-dependency-chain-compromise-over-holidays/
• Sophos: PyTorch machine learning toolkit pwned from Christmas to New Year (3.0.0, 2,386 downloads): https://www.sophos.com/en-us/blog/pytorch-machine-learning-toolkit-pwned-from-christmas-to-new-year
• LWN: Malicious PyTorch dependency (about 2,800 downloads): https://lwn.net/Articles/919119/
• Sonatype: PyTorch namespace (dependency) confusion attack: https://www.sonatype.com/blog/pytorch-namespace-dependency-confusion-attack
• pytorch/pytorch#91539 (torchtriton renamed pytorch-triton): https://github.com/pytorch/pytorch/pull/91539
• pip issue #8606 (index priority): https://github.com/pypa/pip/issues/8606
• uv docs, package indexes (first-index strategy, explicit indexes, tool.uv.sources): https://docs.astral.sh/uv/concepts/indexes/
• npm docs, scopes and associated registries: https://docs.npmjs.com/cli/v10/using-npm/scope
• Alex Birsan: Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies (9 Feb 2021): https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
• PyPI JSON for pytorch-triton and torchtriton, and download.pytorch.org/whl/nightly/torchtriton/, checked by us on 23 Oct 2026; pip 25.3 and uv 0.13.5 resolver test run by us the same day.