Cybersecurity

Nested Package Managers Are a Supply Chain Risk Nobody Tracks

The Trick That Started It All: A Loop Nobody Asked For Engineer Mike Fiedler set out to close a loop. He wanted to find a chain of programming languages where each one’s package manager could install the next one’s runtime. It took two hops. PyPI distributes a Node.js binary through the package nodejs-wheel, and npm ... Read more

Nested Package Managers Are a Supply Chain Risk Nobody Tracks
Illustration · Newzlet

The Trick That Started It All: A Loop Nobody Asked For

Engineer Mike Fiedler set out to close a loop. He wanted to find a chain of programming languages where each one’s package manager could install the next one’s runtime. It took two hops. PyPI distributes a Node.js binary through the package nodejs-wheel, and npm distributes a portable CPython interpreter through @bjia56/portable-python. Run pip install nodejs-wheel, get Node. Run npm install @bjia56/portable-python, get Python back. The cycle has no floor. pip and npm can bootstrap each other indefinitely.

Neither Python’s nor JavaScript’s maintainers designed this. Both packages exist because individual developers faced a narrow, irritating problem — needing Node available inside a Python CI environment, or needing a self-contained Python binary that npm could carry — and solved it cleanly at the package level. The ecosystems allowed it. Nobody flagged it.

Fiedler’s discovery prompted a broader mapping exercise: a matrix with package managers on both axes, tracking which tools can be installed by which other tools across 42 clients. The diagonal of that matrix carries its own signal. When a cell on the diagonal is filled, it means a package manager ships itself — a property called self-hosting. It sounds like an elegant design achievement, the kind of thing engineers mention approvingly at conferences. In practice, it means the trust boundary of a tool extends to include the tool itself, and a compromised version can rewrite its own installer without ever leaving the ecosystem’s normal update path.

The chain Fiedler found also exposes something structural: package managers were built to reason about their own ecosystems, not about foreign runtimes arriving through a side door. When pip installs a Node binary, it validates a Python package. What runs inside that package is outside pip’s threat model entirely. The loop between pip and npm isn’t a bug in either system. It’s what happens when two ecosystems, each internally coherent, develop blind spots at exactly the points where they touch each other.

What Most Coverage Misses: This Is a Supply Chain Story, Not a Novelty Story

When Mike Fiedler mapped the loop between nodejs-wheel on PyPI and @bjia56/portable-python on npm, the tech press ran it as a curiosity — a “cursed” puzzle, a party trick for developers with too much free time. That framing buries the actual story.

Every hop in that chain is a crossed trust boundary. When a Python developer runs pip install nodejs-wheel, they are trusting PyPI to faithfully distribute a Node runtime binary. When a JavaScript developer runs npm install @bjia56/portable-python, they are trusting npm to faithfully distribute a CPython runtime. Neither registry was built for cross-runtime binary distribution. Neither has an attestation model scoped to this use case. PyPI’s Sigstore integration and npm’s provenance tooling were designed to verify that a Python package came from a specific Python build pipeline, or that a JavaScript package came from a specific CI workflow — not to verify that a foreign-language runtime binary is what it claims to be.

That gap is exploitable in a specific and underappreciated way. A compromised package at any single node in this cycle does not just affect one language community. It affects every community downstream. Poison the Node binary in nodejs-wheel, and every project that installs it through pip carries a malicious runtime — one that then executes inside environments where Python, not Node, is the assumed execution context. Security tooling watching for suspicious Node behavior has no reason to be looking inside a pip-installed package. The blast radius multiplies across language ecosystems simultaneously, and the artifact that caused the damage arrives wearing the wrong ecosystem’s label.

This is the definition of a supply chain attack surface: a dependency graph where trust assumptions do not match the actual flow of artifacts. The chain Fiedler demonstrated does not bottom out at two hops. Extending it through tools like uv and Conan shows the pattern repeating across 42 package manager clients. Each additional link is another registry asked to vouch for something outside its original design scope, with no formal mechanism for doing so. The novelty framing is not just incomplete — it actively discourages the question that matters: who is responsible for auditing binaries that cross these boundaries, and what happens when no one is?

The Self-Hosting Diagonal: When a Package Manager Packages Itself

When Mike Fiedler built his cross-referencing matrix of 42 package manager clients, the most structurally alarming entries weren’t the off-diagonal ones — they were the entries sitting on the diagonal itself. A package manager that appears on its own diagonal can install or update itself, which creates an immediate bootstrap trust problem: which version of the tool do you use to verify the version of the tool you’re about to install?

This isn’t an abstract puzzle. Pip has wrestled with bootstrapping edge cases for years. The canonical problem surfaces when a broken or outdated pip version is the only available mechanism to fetch a corrected pip. Python’s ecosystem eventually addressed parts of this through ensurepip, a stdlib module that ships a vendored pip snapshot specifically to escape the circular dependency — an acknowledgment, baked into the language itself, that self-updating installers are dangerous enough to warrant a separate rescue path.

The npm equivalent generates real operational pain in enterprise environments. Teams running air-gapped or strictly versioned Node deployments routinely hit scenarios where npm’s self-update mechanism reaches out to a registry that violates their internal policy, or where the version of npm bundled with a Node release conflicts with the version their internal toolchain expects. The self-hosting diagonal turns what should be a routine update into a sequencing problem with no clean starting point.

The underlying failure mode is what security engineers sometimes call a “trusted updater” vulnerability. If an attacker compromises the package that delivers a self-hosting package manager, they control both the payload and the verification mechanism for every subsequent update. The installer becomes self-certifying. Standard supply chain hygiene — pinning hashes, checking signatures, auditing dependencies — depends on having a trustworthy tool to perform those checks. When that tool is itself delivered and updated through the channel it’s supposed to be auditing, the integrity guarantee collapses to whatever trust existed at the very first installation, usually a curl pipe to a shell script with no verification at all.

Fiedler’s matrix makes this visible by forcing the diagonal into view. Most developers have never looked at the diagonal.

Why This Is Happening Now: Portability Pressure Meets Lazy Infrastructure

The demand for single-command environment setup has quietly normalized a practice that security teams have not caught up with. Tools like uv, Rye, and Bun market themselves on the promise that one install command provisions everything a developer needs — runtime, dependencies, toolchain. That promise requires shipping foreign binaries, and shipping foreign binaries means PyPI now hosts a Node.js distribution called nodejs-wheel and npm hosts a portable CPython under @bjia56/portable-python. Two package managers, each carrying the other’s runtime, each installable through the other’s trusted registry.

CI/CD pipelines accelerated this pattern. Containerless local dev setups and ephemeral build environments punish any workflow that requires pre-provisioned system dependencies. When a GitHub Actions runner needs a Conan build for a C++ project inside a Python pipeline, the path of least resistance is uv tool install conan — which silently pulls in whatever Conan needs — rather than wrestling with system package availability across runner images. Speed wins. The friction-reduction is real and measurable in onboarding time and pipeline reliability.

The incentive structure is the problem. The individual developer gets a faster setup. The organization gets a supply chain that now extends into registries it never explicitly trusted. PyPI’s security model was built around Python packages. It was not built to audit Node binaries or portable C runtimes bundled as wheel files. When a package crosses that boundary — carrying a foreign runtime as a blob — the registry’s existing trust signals become noise. Download counts, maintainer reputation, and signing metadata all apply to the wrapper, not to whatever is compressed inside it.

The chain that Mike Fiedler documented — pip install hands off to npm install, which hands back to pip install — does not require a malicious actor to become a problem. It only requires one compromised blob at any point in that loop. The broader the adoption of batteries-included tooling, the more of these loops exist in production environments, and the fewer engineers can accurately describe what is actually running on their machines after a single install command.

What Needs to Change: Provenance, Policy, and Honest Conversation

PyPI and npm need written policies on cross-runtime binary distribution. Today, packages like nodejs-wheel and @bjia56/portable-python exist in a governance grey zone — they ship compiled foreign-language runtimes through registries whose rules were written for source packages and native extensions, not for bootstrapping entirely separate language ecosystems. Neither PyPI’s Acceptable Use Policy nor npm’s terms explicitly address what it means to vendor a full CPython or Node binary inside a tarball. That silence is a policy failure, not an edge case.

Provenance tooling exists but stops short of where it matters most. Sigstore is live on PyPI and npm supports package provenance attestations, but neither standard currently mandates coverage for packages that bundle compiled foreign runtimes. A signed source wheel tells you where the Python code came from. It says nothing about the pre-built Node binary sitting next to it. Registry maintainers should extend provenance requirements specifically to any package that ships an executable runtime not native to that registry’s language ecosystem — and treat the absence of that attestation as a reason to flag, not publish.

The developer community carries its own obligation. The same engineers who mapped out two-hop install loops between pip and npm, who traced chains from brew install uv through uv tool install conan and back again, treated the result as a clever puzzle. The right frame is an attack surface audit. Every link in those chains is a trust decision made implicitly — a decision about whose build pipeline signed the binary, what that binary can do on the host system, and whether any scanner in a typical CI environment would catch a compromise at that layer.

The creative energy that built these loops should go into documenting their trust assumptions and stress-testing them against realistic threat models. Which packages in the chain have Sigstore attestations? Which were built in reproducible environments? Which hand off execution to a runtime that the installing registry has never audited? Those are answerable questions. Right now, almost nobody is asking them.

AI-Assisted Content — This article was produced with AI assistance. Sources are cited below. Factual claims are verified automatically; uncertain claims are flagged for human review. Found an error? Contact us or read our AI Disclosure.

More in Cybersecurity

See all →