Safeguard
Application Security

The ultimate guide to creating a secure Python package

A concrete, numbers-first guide to locking dependencies, signing releases, and scanning for CVEs when building a secure Python package.

Priya Mehta
DevSecOps Engineer
6 min read

Python's packaging ecosystem shipped more than 604,000 projects on PyPI as of mid-2026, and attackers treat that index as an open door. In 2024 alone, researchers tracked over 7,000 malicious packages uploaded to PyPI, many using typosquatting names like reqeusts or python-dotenv- to catch a single misplaced keystroke in a pip install command. Building a secure Python package is no longer optional hygiene — it is the difference between shipping software your users can trust and becoming the next supply chain incident writeup. This guide walks through the concrete steps: locking dependencies, signing releases, scanning for known vulnerabilities, and structuring your CI/CD pipeline so a compromised maintainer laptop or a leaked PyPI token can't silently poison thousands of downstream builds. Each section below answers the question directly, then shows the commands and configuration that make it real.

What makes a Python package insecure in the first place?

The most common root causes are unpinned dependencies, exposed credentials, and unsigned artifacts. A 2024 Checkmarx analysis found that over 20% of the top 1,000 PyPI packages had at least one transitive dependency with a known CVE that the maintainer had never pinned or patched. Unpinned install_requires entries (e.g., requests>=2.0) let pip silently resolve to whatever the latest matching version is at install time — including a version published five minutes ago by an attacker who compromised a maintainer's account, as happened with ctx and phpass in 2022 when a researcher hijacked abandoned PyPI namespaces. Hardcoded secrets are the second-biggest offender: GitGuardian's 2024 State of Secrets Sprawl report logged over 12.7 million secrets exposed on public GitHub in 2023, and a meaningful share end up baked into published packages via setup.py or .pypirc files accidentally included in the sdist.

How do you lock and verify your dependency tree?

You lock dependencies with a hash-pinned lockfile and verify them with pip install --require-hashes. Tools like pip-tools (pip-compile --generate-hashes requirements.in) or poetry lock produce a requirements.txt or poetry.lock where every package version resolves to an exact SHA-256 hash, not just a version string. This closes the dependency confusion attack vector that hit Apple, Microsoft, and Tesla internal builds in Alex Birsan's 2021 disclosure, where he published public packages matching internal package names and had them installed by CI systems that checked public indexes before private ones. Pair the lockfile with pip-audit (maintained by PyPA) or safety check, both of which cross-reference your resolved tree against the OSV and PyPI Advisory databases. Run pip-audit in CI on every pull request, not just nightly — a dependency published an hour ago with a critical CVE should fail your build before it fails your customer.

Why does signing your package with Sigstore matter?

Signing matters because it lets a downstream user cryptographically verify that the artifact they installed came from your source repository and not a compromised PyPI account. Since May 2023, PyPI has supported Trusted Publishing via OpenID Connect, which lets GitHub Actions (or GitLab CI) publish directly to PyPI using short-lived OIDC tokens instead of long-lived API tokens stored as repo secrets — eliminating the exact attack vector used in the December 2022 PyTorch-nightly compromise, where a malicious dependency exfiltrated SSH keys and environment variables from build machines. Combine Trusted Publishing with Sigstore's sigstore-python to generate a signed provenance attestation for every release; PyPI now displays these attestations directly on the package page. If you maintain a high-traffic package, enable both: Trusted Publishing removes the credential-theft risk, and Sigstore attestation gives consumers (including automated scanners) a verifiable build-to-source link.

What should your CI/CD pipeline actually check before publishing?

Your pipeline should run four gates: dependency audit, static analysis, secret scanning, and SBOM generation — in that order, before the build step. Static analysis with bandit catches Python-specific issues like eval() on untrusted input or subprocess calls with shell=True (both flagged in the CWE Top 25 for 2024, with CWE-78 OS Command Injection ranking at #5). Secret scanning with gitleaks or trufflehog should run on every commit, not just at release time, because a secret committed and force-pushed away is still in the reflog and in any fork that pulled before the rewrite. Generate a Software Bill of Materials in CycloneDX or SPDX format using cyclonedx-bom as your final packaging step, and publish it alongside the release artifact — as of 2024, this is a contractual requirement for any package consumed by U.S. federal agencies under Executive Order 14028's SBOM mandate, and it's increasingly a procurement requirement for enterprise buyers regardless of sector.

How do you handle a vulnerability disclosure after you've shipped?

You handle it by having a SECURITY.md file, a private disclosure channel, and a pre-agreed patch-and-yank process before you need one. PyPI supports package "yanking" (not deletion) via pip install pkg==1.2.3 still resolving for pinned users while pip install pkg skips the yanked version — use this instead of deleting a vulnerable release outright, which breaks reproducible builds for everyone still pinned to it. Request a CVE through GitHub's advisory database (which mirrors to the National Vulnerability Database and to the Open Source Vulnerability schema) so that pip-audit and OSV-based scanners downstream pick up the advisory automatically, typically within 24-48 hours of publication. The python-jwt and Pillow maintainer teams both used this exact flow in 2023-2024 disclosures: private report, coordinated patch release, yank of the vulnerable version, and a GHSA advisory published simultaneously with the fix.

How Safeguard Helps

Safeguard turns the checklist above into continuous, automated coverage instead of a one-time audit. Our reachability analysis traces whether a vulnerable function in a transitive dependency is actually called by your code, so your team can triage the CVE-2024-class alert that matters instead of drowning in the hundreds that don't. Griffin AI, Safeguard's investigation agent, correlates that reachability data with exploit maturity and package provenance to explain — in plain language — why a specific finding is or isn't urgent. Safeguard also generates and ingests CycloneDX/SPDX SBOMs directly from your build pipeline, keeping a live inventory instead of a point-in-time snapshot, and opens auto-fix pull requests that bump the pinned, hash-verified version needed to close a vulnerability without waiting on a manual triage cycle. Together, that means your Python packages ship with the locking, signing, and scanning discipline described above, verified continuously rather than checked once at release.

Never miss an update

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