Back to Blog

Don't Trust Your Dependencies: A Practical Guide to Supply Chain Security

A hands-on strategy against malicious package versions — from dependency cooldowns to lockfile pinning and dev containers.

2026-04-097 min read

In late March 2026, a coordinated supply chain campaign compromised multiple widely-used Python packages within days, including LiteLLM and the Telnyx SDK. Since our team relies on Python across most of our customer projects, we moved quickly to reinforce our setup against such attacks.

This article introduces a layered approach that developers can set up to protect against such attacks. This is becoming more urgent as AI-assisted development tools expand the attack surface: MCP servers, for instance, automatically download and run dependencies via tools like uvx – often without asking the user for permission.

Supply Chain Attacks on PyPI Packages

In late March 2026, a threat group known as TeamPCP executed a coordinated supply chain campaign that hit multiple popular Python packages within days. LiteLLM versions 1.82.7 and 1.82.8 were published to PyPI with a malicious .pth file. What makes such files particularly dangerous is that Python automatically executes them on every interpreter startup. This means any Python process on the affected machine would trigger the credential-stealing script, whether or not it had anything to do with LiteLLM.

Three days later, the Telnyx Python SDK was compromised using a different technique. Malicious code was injected directly into telnyx/_client.py, so it executed silently whenever the package was imported. The payload was hidden inside a WAV audio file fetched from a command-and-control server, which is a technique designed to evade detection.

The attacks are not limited to Python. In the same week, the axios npm package – a popular JavaScript library with over 100 million weekly downloads – was compromised through a hijacked maintainer account. The attacker published versions containing a remote access trojan targeting macOS, Windows, and Linux. The malicious versions were live for about 3 hours before removal.

In all described cases, the attackers didn’t bother creating fake look-alike packages. They compromised real, official packages on their respective package registries by stealing publishing credentials. If you use unpinned dependencies – or if your CI/CD pipeline pulls the latest package version automatically – a single pip install --upgrade or uv lock --upgrade could silently execute malicious code on your machine. The consequences can be severe: stolen cloud-provider credentials, API tokens, SSH keys, database passwords, and environment variables. These attacks also install persistent backdoors, and the stolen credentials often get into the wrong hands. The attacker group partnered with a ransomware group to monetize them through extortion.

Defense Strategies Against Attacks

After these attacks surfaced, we implemented a three-layered defense strategy across our projects. No single layer is a complete safeguard, but together they reinforce each other to create a significantly safer development environment.

Layer 1: Dependency Cooldown — Only Install Packages Older Than 7 Days

This setup is one of the most impactful changes we made, and it’s only a few lines in pyproject.toml. We use uv as our Python package manager, but you might find similar settings in other tools as well.

[tool.uv]
exclude-newer="7 days"

This tells uv to refuse resolving any package version published less than 7 days ago. Consequently, when you run uv lock or uv sync, versions younger than the defined threshold simply don’t exist from the resolver’s perspective.

Why does this matter? Both LiteLLM and Telnyx malicious versions were detected and pulled from PyPI within hours. A 7-day cooldown window means you never install a version that the developer community hasn’t had the time to experiment with. You’re essentially letting others discover the security issue before it reaches your machine. A limitation to keep in mind is that this setup only protects the developer against versions caught within 7 days. For incidents we’ve seen recently, this margin is more than enough, but it’s not a guarantee.

Layer 2: Lockfile Pinning

Lockfile pinning was already part of our workflow before these attacks — we commit uv.lock to version control. This lockfile pins exact versions of every dependency along with their cryptographic hashes. When we run uv sync, uv verifies each downloaded package against the lockfile — if a hash doesn’t match, the install is refused.

In practice, our workflow looks like this:

  1. A developer runs uv lock --upgrade-package=<name> to update a specific dependency

  2. The lockfile diff is reviewed in a pull request — just like a code change

  3. CI runs uv sync --locked, which refuses to install anything that doesn’t match the committed lockfile exactly

This means dependency upgrades are always deliberate and reviewed. No one accidentally pulls a new version by running pip install --upgrade. A known limitation is that the lockfile only protects you if it was generated before the compromise. The moment you run uv lock --upgrade, you’re resolving against PyPI and trusting what comes back. This is where the cooldown introduced in Layer 1 ensures that even at the trust boundary, you’re not pulling anything freshly published. Lockfile pinning is not unique to uv – Poetry achieves the same with poetry.lock, and pip-tools with requirements.txt generated via pip-compile. The principle holds across tooling: commit your lockfile, review its diffs, and do not let your pipeline resolve freely against the registry.

Layer 3: Dev Containers for Blast Radius Containment

Even if something malicious slips through, we wanted to limit what it can access. We set up dev containers across all our projects for this reason. A dev container is a Docker-based development environment defined by configuration files in your repository. Instead of running code directly on your host machine, everything executes inside an isolated container — so even if a dependency is compromised, the blast radius is contained.

The setup consists of four files: a devcontainer.json, a docker-compose.yml, a Dockerfile, and a post-create.sh script. When a developer opens the project in VS Code, it auto-detects the .devcontainer/ folder and prompts to reopen inside the container. From there, everything your project contains lives inside the container.

What stays protected if a dependency is compromised:

  • Your host filesystem outside the project directory

  • Personal SSH keys, cloud credentials, and browser sessions stored on your machine

  • Other projects and their secrets

  • Host-level processes (a fork bomb inside the container can’t crash your laptop)

If your container gets compromised, you can destroy it, rotate the secrets that were inside it, and your host machine stays protected.

Summary

Beyond the three layers, we also follow a few straightforward practices: we review diffs before upgrading any dependency, we use uv sync --locked in CI to prevent any resolution from happening during builds, and we never auto-update to the latest version in production pipelines.

The attacks described in this article came through the trusted packages you probably use on a daily basis. That’s what makes supply chain attacks particularly dangerous. No single layer we described is a complete solution, but together they narrow the window of vulnerability and contain the potential damage. The initial setup takes about an hour, but the time invested returns multi-fold.

Want to discuss this topic?

Our team is ready to help you navigate the complexities of AI adoption.

Get in Touch