How to Use pip-audit to Scan Python Packages for Vulnerabilities?
Problem
I ran pip install flask on a fresh project. Three weeks later, I discovered Flask 0.5 had two known CVEs that had been patched years ago. My application had been vulnerable the entire time.
Here’s what pip-audit showed me:
$ pip-auditFound 2 known vulnerabilities in 1 package
Name Version ID Fix Versions---- ------- -------------- ------------Flask 0.5 PYSEC-2019-179 1.0Flask 0.5 PYSEC-2018-66 0.12.3Two vulnerabilities. One package. And I had no idea until I accidentally ran a security scan.
Why did this happen?
I had been installing Python packages the same way for years:
pip install flaskpip install requestspip install pandasI never checked if these packages had known security issues. I assumed PyPI was safe. I assumed popular packages were maintained. I assumed someone else was watching for vulnerabilities.
None of those assumptions were true.
The Python ecosystem has experienced multiple supply chain attacks:
- Malicious packages uploaded with typosquatted names
- Compromised maintainer accounts
- Dependencies with known CVEs that remain unpatched
The telnyx incident was a wake-up call. A legitimate package was compromised, and developers who had installed it got malware. pip-audit wouldn’t catch novel supply chain attacks, but it would catch the thousands of known CVEs sitting in requirements files everywhere.
What is pip-audit?
pip-audit is a command-line tool that checks your Python dependencies against the Python Packaging Advisory Database. It identifies packages with known vulnerabilities and tells you which versions fix them.
The key insight: pip-audit checks known CVEs. It cannot catch zero-day exploits or novel supply chain attacks. But it catches the documented, fixable vulnerabilities that most developers ignore.
How to use pip-audit
Basic installation
python -m pip install pip-auditWhy use python -m pip instead of pip? It ensures pip-audit installs in the same Python environment you’re auditing.
Scan your current environment
pip-auditThis audits everything installed in your current Python environment. pip-audit resolves transitive dependencies automatically, so it catches vulnerabilities in packages you didn’t directly install.
Scan a requirements file
pip-audit -r requirements.txtThis is the safer approach. You audit before installing, not after. You catch vulnerabilities before they reach your environment.
Output to different formats
pip-audit -r requirements.txt --format json > audit.jsonpip-audit -r requirements.txt --format markdown > AUDIT.mdJSON is useful for CI pipelines. Markdown is useful for documentation.
What happens when vulnerabilities are found
When pip-audit finds issues, you get a report like this:
Name Version ID Fix Versions Description---- ------- -------------- ------------ -----------jinja2 2.4 PYSEC-2021-142 2.11.3 Jinja2 is affected by...flask 0.5 PYSEC-2019-179 1.0 Flask before 1.0...requests 2.0 PYSEC-2023-123 2.31.0 requests is affected by...The important columns:
- ID: The vulnerability identifier (links to details in the advisory database)
- Fix Versions: The versions that patch this vulnerability
To fix, upgrade to a safe version:
pip install flask==1.0pip install jinja2==2.11.3pip install requests==2.31.0Then run pip-audit again to confirm:
$ pip-auditNo known vulnerabilities foundIntegrating pip-audit into CI/CD
One-time scanning is worthless. New CVEs emerge constantly. The advisory database updates daily. You need continuous scanning.
GitHub Actions integration
name: Security Audit
on: [push, pull_request]
jobs: pip-audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11'
- name: Run pip-audit with: inputs: requirements.txtThis runs pip-audit on every push and pull request. If vulnerabilities are found, the CI fails. Vulnerable code cannot merge.
Pre-commit hook
repos: - repo: https://github.com/pypa/pip-audit rev: v2.9.0 hooks: - id: pip-audit args: ["-r", "requirements.txt"]Install the hook:
pip install pre-commitpre-commit installNow pip-audit runs before every commit. You catch vulnerabilities locally before they reach the repository.
Scheduled cron jobs for continuous monitoring
CVE databases lag supply chain attacks by 24-72 hours. A vulnerability might be published after your last deployment. Scheduled scans catch these.
# Edit crontabcrontab -e
# Add daily scan at 2 AM0 2 * * * cd /path/to/project && pip-audit -r requirements.txt >> /var/log/pip-audit.log 2>&1Or with email notification:
0 2 * * * cd /path/to/project && pip-audit -r requirements.txt | mail -s "pip-audit report" [email protected]Why pip-audit alone is not enough
pip-audit checks known CVEs. It cannot catch:
- Novel supply chain attacks (package compromised but CVE not yet published)
- Malicious code in legitimate packages (no CVE exists yet)
- Typosquatted packages (looks like a real package but isn’t)
A Reddit discussion highlighted this:
“pip-audit plus locking everything. The telnyx thing is a reminder that CVE scanning cannot catch novel supply chain attacks”
The solution: pip-audit + dependency locking + hashed requirements.
Lock dependencies with hashes
Flask==2.3.0 \ --hash=sha256:123abc... \ --hash=sha256:456def...requests==2.31.0 \ --hash=sha256:789ghi...Hashed requirements ensure you install exactly what you tested. Even if PyPI is compromised, the hash won’t match.
Generate hashes with pip-tools:
pip install pip-toolspip-compile requirements.in --generate-hashesCommon mistakes
Mistake 1: One-time scanning only
# Initial setuppip-audit
# Then never run it again...This misses CVEs published after your initial scan. The advisory database updates constantly.
Fix: Schedule daily or weekly scans. Integrate into CI.
Mistake 2: Ignoring transitive dependencies
Your requirements.txt: flask==2.0
Flask depends on: Jinja2 Werkzeug itsdangerous
If Jinja2 has a CVE, Flask is vulnerable too.pip-audit resolves transitive dependencies by default. Don’t override this with --no-deps unless you’re scanning hashed requirements.
Mistake 3: Not pinning versions
# requirements.txtflask # Bad: installs latest, changes over timerequests # BadUnpinned versions mean:
- Different developers get different versions
- CI might install a different version than production
- A “safe” version today might be vulnerable tomorrow
Fix: Pin to exact versions:
flask==2.3.0requests==2.31.0jinja2==3.1.2Mistake 4: Blindly ignoring vulnerabilities
pip-audit --ignore-vuln PYSEC-2019-179 --ignore-vuln PYSEC-2018-66Sometimes you cannot upgrade immediately. But ignoring without investigation is dangerous.
Fix: Document each ignored vulnerability:
PYSEC-2019-179 (Flask): - Reason: Cannot upgrade to 1.0 due to breaking API changes - Impact assessment: Our app does not use affected feature - Remediation timeline: Upgrade scheduled for Q2 2026 - Approved by: security-teamMistake 5: Skipping CI integration
# Developer remembers to run pip-audit sometimespip-audit
# But usually forgets...Manual processes fail under deadline pressure. Developers skip security checks when rushing.
Fix: CI integration enforces scanning on every PR. Vulnerable code cannot merge.
Architecture for defense in depth
pip-audit is one layer. A complete security strategy needs multiple layers:
Layer 1: pip-audit (known CVEs) ├── CI integration (every PR) ├── Scheduled scans (daily/weekly) └── Pre-commit hooks (local check)
Layer 2: Dependency locking ├── Pin exact versions ├── Hash requirements └── No unpinned dependencies
Layer 3: Supply chain verification ├── Verify package signatures ├── Check maintainer reputation ├── Monitor dependency changes
Layer 4: Runtime protection ├── Container scanning ├── Network segmentation ├── Audit loggingpip-audit handles Layer 1. You need the other layers for complete protection.
Alternatives to pip-audit
uv audit (faster, Rust-based)
pip install uvuv audituv is significantly faster than pip. If you have large dependency trees, uv audit completes in seconds while pip-audit takes minutes.
Safety (commercial + free tier)
pip install safetysafety checkSafety uses a proprietary vulnerability database. The free tier is limited. Commercial plans include more CVEs and continuous monitoring.
Quick reference
# Installpython -m pip install pip-audit
# Basic scanpip-audit # Scan current environmentpip-audit -r requirements.txt # Scan requirements file
# Output formatspip-audit --format json # JSON outputpip-audit --format markdown # Markdown output
# Fix vulnerabilitiespip install package==safe-version # Upgrade to fix version
# Ignore specific vulnerability (use sparingly)pip-audit --ignore-vuln PYSEC-XXXX
# Multiple requirements filespip-audit -r requirements.txt -r requirements-dev.txt
# Scan hashed requirements (no dependency resolution)pip-audit -r requirements.txt --no-depsSummary
pip-audit is essential for Python security. It scans dependencies against the Python Packaging Advisory Database and reports known CVEs.
But pip-audit alone is insufficient. Use it with:
- CI integration (every push, every PR)
- Scheduled scans (catch CVEs published after deployment)
- Dependency locking (pin versions, add hashes)
- Documentation for ignored vulnerabilities
The telnyx incident proved that CVE scanning cannot catch novel attacks. But it catches thousands of known, fixable vulnerabilities that developers otherwise ignore.
Key takeaway
pip-audit automates vulnerability scanning. But automation without enforcement is theater. Integrate pip-audit into CI so vulnerable code cannot merge. Schedule regular scans so CVEs published after deployment are caught. Lock dependencies so you install exactly what you tested.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 pip-audit GitHub Repository
- 👨💻 Python Packaging Advisory Database
- 👨💻 GitHub Action for pip-audit
- 👨💻 pip-audit Documentation
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments