When you run pip install package-name, pip may execute arbitrary Python code from the package before it is installed. This happens through setup.py, build system hooks, and package metadata generation. Unlike npm where install scripts are a configurable feature, Python's build-time code execution is deeply embedded in the packaging ecosystem.
This is not a flaw in pip. It is a design decision that made sense when packages needed to compile C extensions and probe system capabilities during installation. But it has become the primary vector for Python supply chain attacks.
Where Code Executes During pip Install
setup.py execution. For packages that use the legacy setup.py-based build system, pip executes setup.py to determine metadata (name, version, dependencies) and to build the package. A malicious setup.py runs whatever Python code it contains, with full privileges.
pyproject.toml build backends. PEP 517 introduced a new build system interface using pyproject.toml. When pip builds a package from source, it calls the specified build backend (setuptools, flit, poetry-core, hatchling). The build backend itself is trustworthy, but the package's build configuration can still execute arbitrary code through setuptools entry points, custom build hooks, and conftest files.
setup.cfg / pyproject.toml dynamic metadata. Some metadata fields (like version numbers) can be computed dynamically by executing Python code. This is a legitimate feature but creates execution opportunities during metadata resolution.
Extension compilation. Packages with C extensions run the compiler and linker during installation. While the compilation itself is not usually the attack vector, the build scripts that drive compilation can be.
The Attack Pattern
A typical Python supply chain attack through pip:
- Attacker creates a malicious package on PyPI (typosquat, dependency confusion, or account takeover).
- The package includes a
setup.pywith malicious code in theinstallclass or at module level. - Victim runs
pip install malicious-package. - pip downloads the source distribution and runs
setup.pyto determine metadata. - Malicious code executes: steals environment variables, downloads a payload, establishes persistence.
The malicious code often runs during metadata resolution, before pip even decides whether to install the package. Just resolving dependencies can trigger execution.
Wheels as Mitigation
Python wheels (.whl files) are pre-built packages that do not require setup.py execution. When pip installs a wheel, it extracts the files without running any code from the package.
This makes wheels inherently safer than source distributions. If a package publishes wheels for your platform, pip will prefer the wheel and skip setup.py execution entirely.
However:
- Not all packages publish wheels
- Wheels may not be available for all platforms
- pip falls back to source distributions when wheels are unavailable
pip install --no-binary :all:forces source builds
An attacker who wants to force setup.py execution can publish a package without wheels, or target platforms for which no wheel is published.
Defenses
Prefer wheels. Use --only-binary :all: to refuse source distributions entirely. This breaks packages that do not publish wheels but eliminates setup.py execution.
Use a private index. Host approved packages on a private PyPI server. Scan incoming packages for malicious setup.py patterns before making them available.
Pin and hash. Use pip install --require-hashes with a requirements file that includes hashes for specific wheel files. This pins to exact artifacts and refuses any package without a matching hash.
Build isolation. pip's build isolation (enabled by default for PEP 517 builds) creates a temporary virtual environment for the build process. This provides some isolation but does not prevent network access or filesystem reads.
Review setup.py for new dependencies. Before adding a new Python dependency, examine its setup.py or build configuration. Check for suspicious code: base64 decoding, network requests, subprocess calls, file reads outside the package directory.
Use pip-audit. pip-audit checks installed packages against the OSV database for known vulnerabilities. Run it in CI after installation to catch known-malicious packages.
The Ecosystem Direction
The Python packaging ecosystem is moving toward declarative metadata (PEP 621, pyproject.toml) that does not require code execution for metadata resolution. Build backends like flit and hatchling support fully declarative configuration.
But setuptools, the most widely used build backend, still supports setup.py and dynamic metadata. The transition away from executable installation hooks will take years.
PEP 740 (Trusted Publishers and attestations) adds provenance verification to PyPI, linking packages to their CI/CD pipelines. When fully adopted, this will make it harder to publish malicious packages without detection.
How Safeguard.sh Helps
Safeguard.sh monitors your Python dependencies for known malicious packages, newly published suspicious versions, and vulnerability disclosures. Our platform scans your dependency tree and alerts you when a package with a risky setup.py pattern or known supply chain compromise is detected. Combined with SBOM generation that captures your full Python dependency graph, Safeguard.sh provides the visibility needed to manage the risks inherent in Python's packaging model.