Minimum Sanity Rules for the Age of Supply Chain Attacks
TL;DR: The recent LiteLLM and Axios compromises—where cascading supply chain attacks turned widely-used packages into credential stealers and RAT droppers within hours—are a reminder that basic security hygiene is not optional. This post summarizes what happened and proposes a short list of minimum sanity rules: sandbox untrusted code (especially MCP servers), encrypt secrets at rest with SOPS, protect your SSH keys—from passphrases through OS keychains to hardware tokens and Secure Enclave keys—pin your dependencies, and set minimum release age policies so freshly published malicious versions never reach your machines.
What happened with LiteLLM
On March 24, 2026, versions 1.82.7 and 1.82.8 of the LiteLLM Python package on PyPI were replaced with trojanized builds containing a multi-stage credential stealer. LiteLLM is a popular LLM API gateway with roughly 3.4 million downloads per day and, according to Wiz, present in about 36% of cloud environments. Its entire purpose is to hold API keys for AI providers—OpenAI, Anthropic, Google, Azure, and others—making it a high-value target.
The attack was part of a broader campaign that unfolded over several weeks. The chain of events is worth spelling out:
- On February 27, an AI-powered agent exploited a misconfigured CI trigger (
pull_request_target) in Aqua Security’s Trivy repository to steal a Personal Access Token. - Over the following weeks, the attackers used those credentials to compromise Trivy’s GitHub Actions, Docker images, and release tags.
- On March 24, LiteLLM’s CI/CD pipeline ran the compromised Trivy scanner, which exfiltrated the PyPI publishing token and GitHub credentials.
- The attackers used the stolen token to publish two malicious LiteLLM versions directly to PyPI, bypassing GitHub’s release process entirely.
The malware harvested SSH keys, environment variables, cloud provider credentials (AWS, GCP, Azure), Kubernetes secrets, database passwords, cryptocurrency wallets, CI/CD tokens, and shell history. Everything was encrypted with AES-256-CBC, bundled, and exfiltrated via HTTPS to a typosquatting domain registered the day before.
Within 46 minutes, the malicious versions had been downloaded roughly 47,000 times.
Here is the remarkable part: the compromise was discovered by accident. Callum McMahon at FutureSearch was testing an MCP server that pulled LiteLLM as a transitive dependency. When Cursor launched the MCP server via uvx, it silently downloaded the compromised package—no user prompt, no approval step. The malicious .pth payload created an unintended fork bomb—each spawned Python subprocess re-triggered the payload, spawning over 11,000 processes—causing RAM exhaustion. A bug in the malware is what drew attention. The window between publication and PyPI quarantine was roughly four hours.
When the community opened GitHub issue #24512, the attackers posted 88 bot comments from 73 compromised developer accounts in a 102-second window to flood the discussion, then used the hijacked maintainer account to close the issue as “not planned”. This was not subtle.
What happened with Axios
While this post was being written, the same pattern struck again—this time on the JavaScript side, at even larger scale.
On March 31, 2026, two malicious versions of Axios—the most popular HTTP client library on npm with roughly 100 million weekly downloads—were published: axios@1.14.1 and axios@0.30.4. The attacker compromised the npm account of a lead maintainer (jasonsaayman), changed the registered email to a Proton Mail address under their control, and used a stolen long-lived npm access token to publish directly to the registry, bypassing the project’s normal GitHub Actions CI/CD pipeline with OIDC Trusted Publisher binding. There were no corresponding GitHub commits, tags, or releases.
The sole change in both versions was the addition of a new dependency: plain-crypto-js@^4.2.1. This package is never imported anywhere in the Axios source code. Its only purpose was to execute a postinstall script that acted as a cross-platform remote access trojan (RAT) dropper, targeting macOS, Windows, and Linux:
-
macOS: A binary disguised as an Apple cache daemon at
/Library/Caches/com.apple.act.mond. -
Windows: A PowerShell script executed via a hidden VBScript, with the interpreter copied to
%PROGRAMDATA%\wt.exe. -
Linux: A Python script at
/tmp/ld.py.
The dropper contacted a live command-and-control server (sfrclak[.]com), delivered platform-specific second-stage payloads, then erased itself and replaced its own package.json with a clean decoy—leaving developers who inspected node_modules after the fact with no indication anything had happened.
The attack was staged carefully. A clean decoy version (plain-crypto-js@4.2.0) was published roughly 18 hours before the malicious 4.2.1 to establish legitimate registry history. The trojanized Axios versions appeared within 39 minutes of each other, hitting both the 1.x and legacy 0.x release lines. Socket’s automated malware detection flagged the package within six minutes.
The LiteLLM and Axios incidents are structurally identical: compromise a maintainer’s publishing credentials, push a malicious version through the official registry, and rely on the fact that millions of downstream consumers will install latest without question. The payload differs (credential stealer vs. RAT), but the vector is the same. Rule 5 below—setting minimum release age policies—would have prevented both from reaching any machine with the policy in place.
What is a supply chain attack?
If you are not from a security background, the concept is straightforward. Think of municipal drinking water. You turn on the tap and trust that the water is clean, because it has been treated, tested, and certified by your water utility. You do not personally verify each glass. A supply chain attack is someone poisoning the water at the treatment plant. Every downstream consumer is affected, and nobody suspects the tap.
In software, the “treatment plant” is the package registry—PyPI, npm, Docker Hub—and the “certified clean” label is the fact that a package is published by a known maintainer, has a version number, passes checksums, and comes through the official distribution channel. You run pip install litellm and trust that what you get is what the developers intended.
The LiteLLM case makes this viscerally clear. The .pth file in version 1.82.8 was correctly declared in the wheel’s RECORD file with a matching hash. The wheel was well-formed; standard integrity checks would not have flagged anything. Even pip install --require-hashes would not have helped unless you had pinned to a previous known-good version—the attacker’s wheel has a perfectly valid hash, it is just not the hash of the code you expected. (This is exactly why dependency pinning matters; see Rule 4 below.) The malware executed on every Python interpreter startup—not just when you imported LiteLLM, but whenever anything in that environment ran Python, including your IDE’s language server and your CI build steps.
This is why supply chain attacks are qualitatively different from traditional malware. You are not being careless. You are doing exactly what you are supposed to do—installing dependencies through official channels—and the channel itself has been compromised.
Minimum sanity rules
None of the rules below would have prevented the LiteLLM compromise by themselves. But each of them limits the blast radius: the difference between “the attacker got my OpenAI API key from an environment variable” and “the attacker got every credential, SSH key, cloud token, and database password on the machine”.
Rule 1: Run AI agents and untrusted code in sandboxes
This is the single most impactful thing you can do. If the LiteLLM payload had executed inside a locked-down container with no access to the host filesystem, no SSH keys, no cloud credentials, and no Kubernetes configs, the exfiltration would have captured essentially nothing.
The tools exist and they are mature:
-
Docker: The obvious choice. Run your AI agents, development environments, and CI builds in containers with minimal volume mounts. Do not bind-mount your home directory or
~/.sshinto a container unless you have a specific reason. -
Bubblewrap (
bwrap): A lightweight, unprivileged sandboxing tool used by Flatpak. It creates a minimal filesystem namespace and drops capabilities. Useful when Docker is overkill or unavailable. -
Seatbelt (macOS): Apple’s sandbox framework, used by the App Sandbox. If you are on macOS and running agents locally, sandboxing via
sandbox-exec(deprecated but still functional) or proper App Sandbox entitlements limits what a process can touch.
The principle is simple: treat every dependency installation and every agent execution as potentially hostile. You do not need to be paranoid about every package. You just need to ensure that if one of them turns out to be compromised, the damage is contained.
For AI coding agents specifically, this matters more than most people realize. These agents routinely pip install, npm install, and cargo build as part of their workflow. Each of those commands is an opportunity for a supply chain payload to execute. Run the agent in a sandbox and you have a firewall between the agent’s environment and your actual credentials.
The MCP vector deserves special attention. MCP (Model Context Protocol) servers are typically launched by tools like uvx (Python) or npx (Node.js). Both of these executors silently resolve and download the full dependency tree at startup—no confirmation dialog, no approval step. This is exactly how FutureSearch got hit: Cursor loaded an MCP server, uvx pulled litellm as a transitive dependency, and the payload executed before anyone had a chance to notice. As FutureSearch pointed out, no prompt injection was needed—the attack happened at the package level, below the LLM entirely. If your MCP servers run inside a container with no access to your host credentials, this entire class of attack becomes a non-event.
SSH agent forwarding into containers
A common objection to sandboxing is: “But I need SSH inside the container—for git push, for deploying, for accessing private repos.” The temptation is to bind-mount ~/.ssh into the container, which defeats the purpose entirely. The better approach is to forward the host’s ssh-agent socket into the container. The container can request signatures from the agent, but the private key material never enters the container’s filesystem.
Docker (Linux). The SSH_AUTH_SOCK socket can be mounted directly:
docker run -it \
-v $SSH_AUTH_SOCK:/run/ssh-agent.sock \
-e SSH_AUTH_SOCK=/run/ssh-agent.sock \
my-dev-image
The container process talks to the host agent through the socket. No key files, no passphrases, nothing to exfiltrate from disk.
Docker (macOS). Docker Desktop for Mac does not share Unix sockets natively between the host and the Linux VM. Docker provides a special magic path for this:
docker run -it \
-v /run/host-services/ssh-auth.sock:/run/ssh-agent.sock \
-e SSH_AUTH_SOCK=/run/ssh-agent.sock \
my-dev-image
Using socat for more complex setups. If you need to relay the agent socket across a network boundary (e.g., into a remote VM or a container runtime that does not support socket bind-mounts), socat can bridge a Unix socket to a TCP port and back:
# On the host: expose the agent socket on a TCP port
socat TCP-LISTEN:2222,bind=127.0.0.1,fork \
UNIX-CONNECT:$SSH_AUTH_SOCK
# Inside the container: recreate the socket from the TCP port
socat UNIX-LISTEN:/run/ssh-agent.sock,fork \
TCP:host.docker.internal:2222
export SSH_AUTH_SOCK=/run/ssh-agent.sock
This adds a TCP hop, so make sure the port is bound to 127.0.0.1 (or the Docker bridge) and not exposed to the network. The point is that even in setups where direct socket mounting is not available, there is no reason to copy private keys into the container.
If you combine agent forwarding with a hardware token or Secure Enclave key (Rule 3 below), the guarantee is even stronger: the agent can produce signatures, but the private key cannot be extracted from the agent either, because it lives in hardware. A compromised container process that talks to the forwarded socket can use the key for the duration of the session, but it cannot steal it.
Rule 2: Encrypt secrets at rest with SOPS
Environment variables and .env files are the most common way developers store API keys, database credentials, and other secrets. They are also the first thing any credential stealer harvests—the LiteLLM payload explicitly targeted environment variables and .env files.
SOPS (Secrets OPerationS, originally by Mozilla) encrypts secret files at rest using cloud KMS (AWS KMS, GCP KMS, Azure Key Vault) or PGP/age keys. The encrypted file can be committed to version control safely. Secrets are only decrypted when explicitly needed by an authorized process.
The key advantage over plain .env files: even if an attacker exfiltrates the file, they get ciphertext. Without access to the decryption key (which lives in your KMS, keychain, or vault, not on disk), the secrets are useless.
This is conceptually straightforward:
- Store secrets in a SOPS-encrypted file instead of plaintext
.envfiles. - Decrypt at runtime only in the process that needs the secret.
- Never export decrypted secrets into the shell environment of a broader session.
The last point matters. If you decrypt a SOPS file and then export everything into your shell, a supply chain payload running in that shell can still read the environment variables. The goal is to minimize the window and scope of exposure.
Encrypted disk images for high-value secrets
SOPS works well for individual secret files, but sometimes you have an entire directory of high-value material—private keys, signing certificates, recovery codes, credentials for critical infrastructure—that you want behind a separate layer of encryption with its own password, independent of your login credentials.
On macOS, you can create an encrypted sparse disk image using Disk Utility or the command line. On macOS 26 (Tahoe) and later, prefer the new Apple Sparse Image Format (ASIF)—it is dramatically faster than the legacy format (encrypted read/write speeds of ~4.5 GB/s vs. <100 MB/s on the old UDSP format) while remaining sparse (the file on disk grows only as you add data):
# macOS 26+: create an encrypted ASIF image via diskutil
diskutil image create blank ~/Vault.sparseimage \
--fs APFS --format ASIF --size 256MiB --encryption
# Pre-Tahoe: create a legacy encrypted sparse image via hdiutil
hdiutil create -size 256m -encryption AES-256 -type SPARSE \
-fs APFS -volname "Vault" ~/Vault.sparseimage
You will be prompted for a password. Alternatively, you can do all of this through Disk Utility.app (File > New Image > Blank Image, then choose “sparse image” and enable encryption)—no terminal required. The result is a file on disk that looks like opaque ciphertext. To access the contents, you mount it (double-click in Finder or via the command line):
# Mount (prompts for password)
hdiutil attach ~/Vault.sparseimage
# Your secrets are now at /Volumes/Vault/
# ... do your work ...
# Unmount when done
hdiutil detach /Volumes/Vault
While unmounted, the image is just an encrypted blob—a supply chain payload that scans your filesystem sees nothing usable. While mounted, the contents are accessible to your user, so mount only when needed and unmount when done. The key advantages:
- Separate password: even if your login session is compromised, the vault stays locked unless the attacker also has the vault password.
-
Portable: you can back up or sync the
.sparseimagefile (e.g., to an external drive) without worrying about exposing the contents. - No special tooling: built into macOS, works with Finder and the command line alike.
On Linux, the equivalent is a LUKS-encrypted loop device or a VeraCrypt volume. The principle is the same: a file that becomes a filesystem only when you explicitly unlock it.
This is defense in depth applied to your most sensitive material. SOPS encrypts individual secrets. Encrypted disk images encrypt everything in a directory behind a separate credential boundary. Use both where it matters.
Rule 3: Protect SSH keys with passphrases
This is the oldest advice in the book and still widely ignored. The LiteLLM payload harvested SSH keys and configs. If your private key sits on disk without a passphrase, the attacker has it and can use it immediately.
An SSH key with a passphrase is encrypted at rest. The attacker can exfiltrate the file, but without the passphrase they cannot use it. Combined with ssh-agent (which holds the decrypted key in memory only for your session), this adds a real barrier at essentially zero cost to your workflow.
# Generate a new key with a passphrase (you will be prompted)
ssh-keygen -t ed25519 -C "your_email@example.com"
# Add to ssh-agent (decrypts once, in memory only)
ssh-add ~/.ssh/id_ed25519
If you have existing keys without passphrases, you can add one retroactively:
ssh-keygen -p -f ~/.ssh/id_ed25519
This is encryption at rest applied to your most sensitive authentication material. It costs you typing a passphrase once per session. The same principle from Rule 2 applies here: make sure that what the attacker can grab from disk is ciphertext, not cleartext.
But passphrases are only the first step. The real question is: where does the decrypted key material live, and what can access it? With a plain ssh-agent, the decrypted key sits in the agent’s memory for the duration of your session. Any process running as your user can talk to the agent’s socket and request signatures. That is still a large surface. The next steps on the ladder—keychains, hardware tokens, and secure enclaves—progressively shrink that surface.
Keychains: let the OS manage your secrets
Instead of typing your passphrase every time (or leaving it in a long-lived agent), you can delegate passphrase storage to the operating system’s credential manager. The OS stores the passphrase in an encrypted, access-controlled database that is unlocked by your login session, and hands it to ssh-agent when needed.
macOS Keychain. Since macOS Sierra, the SSH agent integrates directly with the system Keychain. Add the following to ~/.ssh/config:
Host *
UseKeychain yes
AddKeysToAgent yes
With this in place, the next time you use an SSH key, macOS will prompt for the passphrase once and store it in the login keychain—encrypted by the Secure Enclave’s key hierarchy and unlocked automatically when you log in. No manual ssh-add needed; the passphrase is now protected by macOS’s keychain encryption rather than sitting in plaintext in a shell script or .bashrc.
Linux: GNOME Keyring and KDE Wallet. On Linux desktops, GNOME Keyring and KDE Wallet serve the same role. GNOME Keyring includes an SSH agent that automatically unlocks keys whose passphrases are stored in the keyring. KDE Wallet integrates similarly via ksshaskpass. For headless servers, pass with GPG or secret-tool (part of libsecret) can store secrets in an encrypted store.
The pattern is the same in all cases: stop putting secrets where arbitrary processes can read them; put them behind an OS-level credential manager that mediates access.
Hardware tokens: the private key never touches disk
A passphrase-protected key is encrypted at rest, but it still exists as a file on your disk. The attacker exfiltrates the ciphertext and can attempt to brute-force the passphrase offline. A stronger guarantee is to ensure the private key never exists on the filesystem at all.
FIDO2 security keys (YubiKey, SoloKey, Nitrokey, Google Titan, etc.) do exactly this. The private key is generated on the hardware token and never leaves it. The token performs the cryptographic signing operation internally; the host only ever sees the public key and the resulting signature. OpenSSH has supported FIDO2 keys natively since version 8.2:
# Generate an ECDSA key backed by a FIDO2 hardware token
ssh-keygen -t ecdsa-sk -C "your_email@example.com"
# Or Ed25519 if your token supports it (YubiKey 5+)
ssh-keygen -t ed25519-sk -C "your_email@example.com"
The -sk suffix stands for “security key”. The resulting private key file (id_ecdsa_sk) is a handle—it contains the key identifier and metadata, but the actual private key material lives on the token. Without the physical token plugged in, the handle is useless.
For an additional layer, you can make the key resident on the token (also called a discoverable credential), which means even the handle is not needed on disk—you can reconstruct it from the token alone:
# Resident key: no private key file needed on disk at all
ssh-keygen -t ed25519-sk -O resident -C "your_email@example.com"
# Later, on any machine: download handles from the token
ssh-keygen -K
This is the strongest posture against credential theft: there is nothing on disk to exfiltrate.
Secure Enclave keys: the hardware token built into your machine
If you are on a Mac with Apple Silicon (or a T2 chip), you already have a hardware security module: the Secure Enclave. The Secure Enclave generates and stores cryptographic keys in dedicated hardware, isolated from the main processor and the operating system. The private key cannot be read out—not by root, not by malware, not by a kernel exploit. All signing operations happen inside the enclave.
Since macOS Sequoia and OpenSSH 9.x with Apple’s patches, you can generate SSH keys backed by the Secure Enclave:
# Generate a Secure Enclave-backed key (requires Touch ID)
ssh-keygen -t ecdsa-sk -O resident -O application=ssh:YourLabel
Each use of the key requires biometric confirmation via Touch ID. The private key material is physically unreachable from software—even if an attacker has root access to your machine, they cannot extract it.
If you prefer a polished GUI over command-line setup, Secretive by Max Goedjen does exactly this with a clean interface. It acts as an SSH agent that generates and stores keys directly in the Secure Enclave, provides Touch ID (or Apple Watch) confirmation on every use, and shows notifications whenever a key is accessed. The private key never exists as a file—Secretive manages the entire lifecycle inside the enclave. For older Macs without a Secure Enclave, it falls back to smart card-based signing (e.g., YubiKey). It is probably the lowest-friction way to get Secure Enclave SSH keys on macOS today.
On Linux, the equivalent is a TPM 2.0-backed key via tpm2-pkcs11 or FIDO2 tokens as described above. The principle is identical: the secret lives in hardware that refuses to export it.
The hierarchy
It is useful to think of these options as a hierarchy of increasing protection:
| Level | Mechanism | What an attacker with disk access gets |
|---|---|---|
| 0 | No passphrase | The private key, immediately usable |
| 1 | Passphrase | Ciphertext (offline brute-force possible) |
| 2 | Keychain | Ciphertext + OS-mediated access (brute-force requires breaking the keychain) |
| 3 | Hardware token / Secure Enclave | Nothing usable—private key never on disk |
You do not need to jump to Level 3 overnight. But you should not be at Level 0. Moving from 0 to 1 costs you a single ssh-keygen -p command. Moving from 1 to 2 costs you two lines in ~/.ssh/config. Each step strictly reduces what a supply chain attacker gets from reading your filesystem.
Rule 4: Pin dependencies and use lock files
Rules 1–3 limit the blast radius after malicious code executes. This rule tries to prevent execution in the first place.
When you run pip install litellm or uvx some-mcp-server, the package manager resolves the latest version from the registry. If that latest version was published by an attacker ten minutes ago, you get the attacker’s code. This is exactly what happened: uvx resolved litellm 1.82.8, the newest version, and executed it.
The fix is to pin your dependencies to known-good versions and verify them with hashes:
# requirements.txt with pinned version and hash
litellm==1.82.6 \
--hash=sha256:<known-good-hash>
With a hash-pinned requirements file, pip install --require-hashes will refuse to install a version that does not match—even if a newer version exists on PyPI. For MCP servers launched via uvx, you can pin in a pyproject.toml or use a wrapper script that installs from a lock file before launching the server.
The broader ecosystem has mature tooling for this:
-
Python:
pip-compile(from pip-tools) oruv pip compilegenerates a fully pinned lock file with hashes. Poetry and PDM do the same. -
Node.js:
package-lock.json(npm) oryarn.lockalready pin exact versions. Runnpm ci(notnpm install) in CI to enforce them. -
Rust:
Cargo.lockpins by default. Commit it for applications.
The practical principle: never let a package manager silently resolve “latest” in an environment that has access to real credentials. If you must run uvx or npx with live resolution, do it inside a sandbox (Rule 1). If you run outside a sandbox, pin and hash-verify everything.
Rule 5: Set minimum release age policies
Rules 4 and 5 are complementary. Pinning (Rule 4) locks you to a known-good version. Minimum release age ensures that even when you do update, you never install a version that was published minutes or hours ago—the window in which a compromised package is most dangerous and least likely to have been detected.
The idea is simple: tell your package manager to ignore any version published less than N days ago. Most supply chain attacks are discovered and pulled within hours to days. A 3–7 day cooldown means the malicious version has already been yanked by the time your tooling considers it. The LiteLLM payload was quarantined within four hours. Socket flagged the Axios trojan within six minutes. In both cases, a one-week minimum age policy would have meant zero exposure.
The major package managers now support this natively:
npm (v11.10+, released February 2026):
# .npmrc
min-release-age=7
This excludes any version published less than 7 days ago from resolution. You can also pass it on the command line (npm install --min-release-age=7).
pnpm (minimumReleaseAge):
# .npmrc
minimum-release-age=10080
Note: pnpm specifies the value in minutes (10,080 minutes = 7 days).
Bun (minimumReleaseAge):
# bunfig.toml
[install]
minimumReleaseAge = 604800
Note: Bun specifies the value in seconds (604,800 seconds = 7 days). Yes, every tool picked a different unit.
uv (Python, exclude-newer):
# pyproject.toml
[tool.uv]
exclude-newer = "7 days"
This instructs uv lock and uv sync to ignore any PyPI version whose upload-time (per PEP 700) is less than 7 days ago. It also accepts an explicit RFC 3339 timestamp for reproducible builds.
pip (v26+, --uploaded-prior-to):
pip install --uploaded-prior-to 2026-03-24 litellm
pip’s flag takes an explicit date rather than a relative duration. It relies on the same PEP 700 upload-time metadata from PyPI.
Practical considerations
- Align with Dependabot / Renovate. If you use automated dependency update bots, configure their cooldown settings to match your minimum age. Otherwise the bot will open PRs for versions your package manager refuses to install.
-
Private registries. The cooldown relies on the
upload-timefield (PEP 700 for Python, registry metadata for npm). Private or self-hosted registries may not populate this field, in which case the setting has no effect for those packages. - Security patches. A cooldown delays all new versions, including legitimate security fixes. For most projects, waiting 3–7 days for a security patch is acceptable. If you need an emergency override, you can temporarily lower the threshold for a specific install or pin the patched version explicitly.
- Choose your window. Three days catches most attacks while keeping you reasonably current. Seven days is more conservative and catches slower-moving incidents. There is no magic number, but anything above zero is a strict improvement over the default of “install whatever was published ten seconds ago.”
The principle: insert a time buffer between publication and installation. Most malicious packages are detected and removed faster than your cooldown window. This is not a guarantee—a sufficiently patient attacker can wait—but it eliminates the entire class of “smash-and-grab” attacks where a compromised version is published, mass-downloaded, and yanked within hours.
The broader pattern
All the rules above are instances of the same underlying idea: reduce the value of what is lying around in cleartext on disk or in environment variables. Supply chain attacks operate by running code in your environment with your permissions. You cannot always prevent the code from running (the whole point is that it looks legitimate), but you can make sure that when it does run, there is less to steal and less to reach.
| Rule | What it protects against |
|---|---|
| Sandbox | Limits what the attacker’s code can access |
| SOPS | Ensures secrets on disk are ciphertext |
| SSH passphrases + keychains | Ensures keys on disk are encrypted or absent |
| Hardware tokens / Secure Enclave | Ensures the private key never exists on disk |
| Dependency pinning | Prevents the malicious version from being installed at all |
| Minimum release age | Ensures freshly published malicious versions are never even considered |
None of this is new. None of this is exotic. But the LiteLLM and Axios incidents—and the broader campaign that compromised Trivy, 47+ npm packages, Checkmarx KICS, and Docker Hub images in a matter of weeks—show that these basics are not universally practiced, and the cost of neglecting them is rising fast.
References
- Wiz: LiteLLM Supply Chain Attack Analysis
- Sonatype: Compromised litellm PyPI Package
- Snyk: How a Poisoned Security Scanner Became the Key to Backdooring LiteLLM
- FutureSearch: Supply Chain Attack in litellm
- FutureSearch: No Prompt Injection Required
- ARMO: The Library That Holds All Your AI Keys Was Just Backdoored
- pip-tools
- SOPS (getsops/sops)
- Bubblewrap (containers/bubblewrap)
- Secretive (maxgoedjen/secretive)
- Aikido: Axios Compromised on npm
- Socket: Axios npm Package Compromised
- StepSecurity: Axios Compromised on npm
- Socket: npm Introduces minimumReleaseAge
- Andrew Nesbitt: Package Managers Need to Cool Down
- LiteLLM Official Security Update
- CVE-2026-33634 (NVD)
Comments