Three Files Ran Before Your App Did: The Boot Path Nobody Owns

Something ran inside your application this morning before your first line of code executed. It was not your code. It was not your framework. And if you asked around the team who put it there, nobody could name it.
On October 21, 2026, the LiteLLM 1.107.0 release on PyPI shipped a credential stealer and a remote-code-execution backdoor across all seven of its wheels. The malicious code was not hidden in a dependency, not in a post-install script, and not in the library's own modules. It arrived as three files dropped straight into site-packages — the directory where Python installs third-party code.
The first of those files, cook.pth, was twelve bytes long. It contained one line: import cook. That is the whole entry point. Because of how Python boots, those twelve bytes meant the stealer ran in every Python process on the host — your application, pip, your editor's language server, and every CI job — including processes that never imported LiteLLM at all.
Why a 12-byte file is a loaded gun
This is not a bug in Python. It is a documented feature that has quietly become one of the softest targets in the supply chain.
When a Python interpreter starts, a module called site runs first. It scans site-packages for files ending in .pth — normally a way to add extra directories to the import path. But any line inside a .pth file that starts with the word import is executed as real code, at startup, with no confirmation and no logging. The site module also automatically imports a file called sitecustomize.py if one exists anywhere on the path.
In the LiteLLM case, the attackers used both hooks. The 12-byte .pth pulled in a 22,252-byte file wrapping roughly 61 KB of compressed Python in a base64 blob. A second file, sitecustomize.py at 25,509 bytes, started a background daemon that every hour fetched a server address from a GitHub file, re-ran the stealer, and downloaded and executed whatever the server returned. The whole thing was a small, self-updating implant living in a directory almost nobody inspects.
Here is the uncomfortable part. Your SBOM says the right thing. Your lockfile says the right thing. Your scanner says the right thing. The package really was named litellm and the version really was 1.107.0. The only artifact that lists the three extra files is the wheel's own RECORD manifest — and essentially no pipeline reads that manifest and compares it to what the package is supposed to ship.
Provenance proves the pipeline, not the payload
The common instinct here is "we verify signatures, so we're fine." That instinct fails in exactly this scenario.
According to LiteLLM's incident report, the attacker held the GitHub CLI credentials of three developers, likely harvested from the logs of an internal LiteLLM deployment that was exposed to the internet. With an organization-owner token, they bypassed branch protection and pointed the private release workflow at a lookalike repository. A second developer's credential then passed the approval gate. The official pipeline did the rest: it built, signed, and published the artifacts.
The change itself was one line. In the source distribution's pyproject.toml, inside the build section, a single setting told the build tool to copy an extra folder into the wheel. No public commit in the upstream repository contains that line or that folder. The signature was valid because the pipeline was legitimate. Attestation tells you which factory made the box. It does not tell you who wrote the shipping label.
What the attacker actually wanted
The stealer's configuration listed sixteen collection modules, and they read like a map of a modern developer's life: SSH keys, shell history, cloud credentials, browser passwords, CI/CD secrets, GPG keys, TLS private keys, and a category called ai_devtools covering Claude Code, Cursor, Copilot, Codex, Gemini CLI, and more.
But read the module named db_litellm. It reads the database connection string from the environment, then dumps the tables holding provider API keys — the credentials of every application sitting behind that proxy. One compromised proxy becomes a skeleton key for an entire fleet of AI features.
That motive is corroborated elsewhere. The same week, Railway disclosed that an unauthorized third party may have accessed encrypted environment variables and, for some of them, obtained the decrypted contents — and described the event as part of a series of attacks targeting platform companies with the aim of compromising AI inference providers. These are separate incidents, but they share a thesis: LLM provider keys are now the highest-value secret in most stacks. They bill real money, they are long-lived, they usually have no scope limit, and they sit in an environment variable that every service in the cluster can read.
The exfiltration endpoint was a lookalike domain served over plain HTTP. That detail matters for what comes next.
The boot path inventory
The practical lesson is not "pin your dependencies." You already do, and pinning would not have helped, because the pinned version was the poisoned one. The lesson is that you need an inventory of the boot path: everything that executes before, during, and around your code's startup. Sort it into three tiers.
Tier 0 — runs before your process exists
- Shell profile files on build and deploy hosts (.bashrc, .zshrc, .profile)
- Container ENTRYPOINT and CMD, plus any wrapper scripts baked into base images
- systemd ExecStartPre units and Kubernetes initContainers
- Git hooks installed by tooling
Tier 1 — runs inside your process at startup
- Python: .pth files containing import lines, sitecustomize.py, usercustomize.py, PYTHONSTARTUP, PYTHONPATH shims
- Node: anything wired into a preinstall or postinstall script, plus native build hooks
- JVM: -javaagent flags, which are arbitrary bytecode injected before main
- Native: LD_PRELOAD and equivalents
Tier 2 — runs alongside you
- Kubernetes admission webhooks that mutate pods
- Sidecars and service-mesh filters
- Editor and browser extensions with broad activation events
You do not need a platform team to start. Two commands give you a first pass on any Python host. Run the interpreter and print the site-packages directories, then list every .pth file in those directories and surface the ones whose contents begin with import. Second, start the interpreter with import timing enabled and a no-op payload; the modules it loads before doing anything at all are your real startup surface. Most engineers are surprised by how long that list is.
Then write down who owns each tier. That is usually where the exercise dies, and it is the most valuable output.
The control that pays for itself
Assume the payload gets in. What stops it from succeeding?
In this attack, the steal would have failed without a successful outbound connection, and that connection went to a lookalike domain over unencrypted HTTP that no legitimate service in the stack had any reason to call. A default-deny egress policy — an allowlist of destinations you actually talk to — breaks that link for every future variant, regardless of how the payload arrived.
Two supporting controls are worth pairing with it. First, stop treating long-lived provider keys as environment variables. Where your cloud supports it, use workload identity or short-lived tokens, scope each key to a single service, and put a hard monthly spend cap on every provider account so a stolen key has a ceiling. Second, treat the LiteLLM proxy database the way you would treat a password vault, because that is what it is.
What this costs you, honestly
Egress allowlists are genuinely painful. They break vendor SDKs, they break telemetry, they break the new thing someone shipped on Friday, and they need a bypass process that does not become the default. If your team cannot staff an allowlist with an owner and an on-call rotation, start with visibility only — log every destination and alert on new ones — and accept that you are watching rather than blocking.
The cheapest control is also the least popular: a cooldown window on automatic dependency upgrades. The malicious LiteLLM release was removed from PyPI within roughly two hours of publication, and the affected Docker images were live for about two and a half hours. A 72-hour delay before adopting a new version of any dependency that runs in production would have avoided this entirely. The trade-off is obvious and real: you also delay legitimate security patches. The resolution is a documented, narrow exception path for urgent CVEs — not an informal habit of upgrading whenever the bot opens a pull request.
The ownership gap
The reason boot paths go unaudited is organizational, not technical. Application teams own application code. Platform teams own images and clusters. Nobody owns the seam between them: the .pth file, the javaagent flag, the shell profile baked into a base image two years ago. It is not a gap in anyone's backlog because it is not in anyone's backlog.
Give it a name and a person. "Startup surface inventory, owned by platform, reviewed quarterly" is a sentence that would have caught this class of attack before it shipped — not because it predicts the specific payload, but because a package that installs a startup hook is unusual, and unusual is the only signal you get before a name like litellm turns against you.
Key Takeaways
- Execution at startup is the real attack surface. A 12-byte .pth file ran a full credential stealer in every Python process on the host, including ones that never imported the compromised package.
- Your scanners were not wrong — they were answering a narrower question. They report package name and version. Nothing reports "this wheel installed three files it has no business installing."
- Signatures prove the factory, not the label. The malicious build was produced and signed by the official pipeline after an attacker redirected it. Provenance is necessary and insufficient.
- Build a boot path inventory in three tiers. What runs before your process, what runs inside it at startup, and what runs alongside it. Assign an owner to each tier or the gap stays unowned.
- Default-deny egress is the highest-leverage control. The exfiltration went to a lookalike domain over plain HTTP. If your stack cannot make that call, the payload becomes a resident failure instead of a breach.
- A 72-hour upgrade cooldown would have prevented this entire incident. Pair it with scoped, short-lived provider keys and hard spend caps, and stop treating the proxy database as anything less than a vault.