Skip to content

How to Check If Your Python Package Is Compromised on PyPI

My machine was stuttering hard. htop was taking 10+ seconds to load, CPU pegged at 100% with no obvious culprit. I thought it was a runaway process or maybe malware from a sketchy download.

Turns out, I had installed a compromised Python package from PyPI.

The LiteLLM versions 1.82.7 and 1.82.8 contained malicious code that executed on every Python process startup. Here’s how I discovered it, and more importantly, how you can check your own environment.

The Discovery

I had installed litellm the day before for a project:

The innocent install
pip install litellm

The next morning, my machine was crawling. I opened htop and noticed Python processes consuming excessive CPU. Strange, since I wasn’t running anything intensive.

I killed the processes, but they reappeared. That’s when I knew something was wrong.

Step 1: Check for Suspicious .pth Files

The malicious payload was hidden in a .pth file. These files execute automatically when Python starts, making them perfect for persistence.

Finding suspicious .pth files
# Find all .pth files in your Python environment
find $(python3 -c "import sysconfig; print(sysconfig.get_paths()['purelib'])") \
-name "*.pth" -exec grep -l "subprocess\|exec\|eval\|base64" {} \;

I ran this and found litellm_init.pth containing obfuscated code. A legitimate .pth file should only contain import paths, not executable code.

Output I got
/usr/local/lib/python3.11/site-packages/litellm_init.pth

Step 2: Verify Version Against Official Sources

The compromised versions (1.82.7 and 1.82.8) had no corresponding GitHub release. This was a major red flag.

Check installed version
pip show litellm | grep Version
Suspicious output
Version: 1.82.8

Then I checked the official repository:

Check GitHub for releases
curl -s https://api.github.com/repos/BerriAI/litellm/releases/latest | jq -r '.tag_name'
Latest actual release
v1.82.6

Version 1.82.8 didn’t exist on GitHub. It was uploaded directly to PyPI, bypassing the normal release process.

Step 3: Compare Package Hashes

Every package on PyPI has published SHA256 hashes. If your installed package has a different hash, it’s been tampered with.

Download and verify hash
pip download litellm==1.82.8 --no-deps -d /tmp
sha256sum /tmp/litellm-1.82.8-*.whl
Compare with PyPI's published hash
curl -s https://pypi.org/pypi/litellm/1.82.8/json | jq '.urls[0].digests.sha256'

If these don’t match exactly, you have a compromised package.

Step 4: Check for Persistence Mechanisms

The LiteLLM attack created persistence through systemd user services:

Check for malicious persistence files
ls -la ~/.config/sysmon/ 2>/dev/null
ls -la ~/.config/systemd/user/sysmon.service 2>/dev/null
What I found
/home/user/.config/sysmon/sysmon.py
/home/user/.config/systemd/user/sysmon.service

These files shouldn’t exist. The attack created a systemd service that runs on login, ensuring the malware persists even after you remove the package.

Step 5: Check Cache Directories

Package managers like uv and pip cache downloads. A malicious package can hide in these caches:

Check uv cache for malicious files
find ~/.cache/uv -name "litellm_init.pth" 2>/dev/null
Check pip cache
pip cache info
find $(pip cache dir) -name "*litellm*" 2>/dev/null

I found the malicious .pth file in my uv cache, which would have reinfected me if I had run uv pip install again.

What the Attack Was Doing

After investigation, I discovered what the malware was harvesting:

Data targeted by the attack
+---------------------------+----------------------------------+
| Target | Location |
+---------------------------+----------------------------------+
| SSH private keys | ~/.ssh/id_rsa, ~/.ssh/config |
| AWS credentials | ~/.aws/credentials |
| GCP credentials | ~/.config/gcloud/ |
| Azure credentials | ~/.azure/ |
| Kubernetes configs | ~/.kube/config |
| Environment variables | All env vars with secrets |
| Crypto wallets | Various wallet file extensions |
+---------------------------+----------------------------------+

This is a classic credential harvesting attack disguised as a legitimate library update.

Automated Security Audit Tools

After this experience, I set up automated checks:

Using pip-audit for vulnerability scanning
pip install pip-audit
pip-audit --desc --aliases
Using safety for dependency checking
pip install safety
safety check
Full environment scan
# Scan for known vulnerabilities
pip-audit
# Check requirements.txt before installing
pip-audit -r requirements.txt

How to Protect Yourself

1. Hash Pinning in requirements.txt

Instead of flexible version specs, pin exact hashes:

requirements.txt with hash pinning
litellm==1.82.6 \
--hash=sha256:abc123... \
--hash=sha256:def456...

This ensures you get the exact package you expect, not a tampered version.

2. Verify Before Installing

Verify package before installation
# Download without installing
pip download package_name==version --no-deps -d /tmp
# Check the hash
sha256sum /tmp/package_name*.whl
# Compare with PyPI
curl -s https://pypi.org/pypi/package_name/version/json | jq '.urls[0].digests.sha256'
# Only install if hashes match
pip install /tmp/package_name*.whl

3. Check for GitHub Releases

Legitimate packages usually have GitHub releases that match PyPI versions:

Quick release check
# Get PyPI version
pypi_version=$(pip index versions package_name 2>/dev/null | head -1 | awk '{print $2}')
# Get latest GitHub release
gh_version=$(curl -s https://api.github.com/repos/owner/repo/releases/latest | jq -r '.tag_name')
echo "PyPI: $pypi_version, GitHub: $gh_version"

If PyPI has versions that don’t exist on GitHub, be suspicious.

4. Monitor Your Environment

Create a simple check script:

security_check.sh
#!/bin/bash
echo "=== Checking for suspicious .pth files ==="
find $(python3 -c "import sysconfig; print(sysconfig.get_paths()['purelib'])") \
-name "*.pth" -exec grep -l "subprocess\|exec\|eval\|base64\|compile" {} \; 2>/dev/null
echo ""
echo "=== Checking for persistence mechanisms ==="
ls -la ~/.config/sysmon/ 2>/dev/null && echo "WARNING: sysmon directory found"
ls -la ~/.config/systemd/user/*.service 2>/dev/null | grep -v "ExpectedService"
echo ""
echo "=== Running pip-audit ==="
pip-audit --desc 2>/dev/null || echo "pip-audit not installed"

Run this periodically or add it to your CI/CD pipeline.

Cleanup Steps

If you find you’ve installed a compromised package:

Complete cleanup
# 1. Remove the package
pip uninstall litellm -y
# 2. Remove persistence mechanisms
rm -rf ~/.config/sysmon/
rm -f ~/.config/systemd/user/sysmon.service
systemctl --user daemon-reload
# 3. Clear caches
pip cache purge
rm -rf ~/.cache/uv/
# 4. Check for .pth remnants
find $(python3 -c "import sysconfig; print(sysconfig.get_paths()['purelib'])") \
-name "litellm_init.pth" -delete
# 5. Rotate all potentially compromised credentials
# - SSH keys
# - Cloud provider credentials
# - API keys stored in env vars
# - Database passwords

Why This Keeps Happening

PyPI’s model trusts package maintainers. If a maintainer’s credentials are compromised (as happened with LiteLLM), malicious versions can be uploaded directly.

Attack vector timeline
+----------------+------------------------+----------------------------+
| Stage | What Happens | Detection Difficulty |
+----------------+------------------------+----------------------------+
| Credential | Attacker gains access | Hard - happens upstream |
| theft | to maintainer account | |
+----------------+------------------------+----------------------------+
| Malicious | Attacker uploads | Medium - no GitHub release |
| upload | compromised version | for the version |
+----------------+------------------------+----------------------------+
| User install | pip install grabs | Easy - but only after |
| | compromised package | fact |
+----------------+------------------------+----------------------------+
| Code execution | .pth file runs on | Trivial - but damage done |
| | every Python start | |
+----------------+------------------------+----------------------------+

The supply chain attack surface is enormous, and most developers don’t verify what they install.

The Reality Check

I consider myself security-conscious. I use a password manager, I’m careful about downloads, I keep my system updated. But one pip install nearly cost me my SSH keys, cloud credentials, and who knows what else.

The steps I’ve outlined above are now part of my workflow. Not because I’m paranoid, but because I learned the hard way that the package you install might not be the package you think it is.

Quick checklist for every pip install:

  • Is there a matching GitHub release for this version?
  • Does the hash match PyPI’s published hash?
  • Are there any suspicious .pth files after installation?
  • Does pip-audit report any vulnerabilities?

If any of these fail, stop and investigate before proceeding.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments