requirements.txt 能安装不等于安全:用 pip-audit 检查 Python 依赖,cpolar 临时分享脱敏报告

pip install -r requirements.txt 没报错,只能证明依赖解析和安装走通了,不能证明这些版本没有已知漏洞。我最近重新梳理 Python 项目的交付检查时,就把"能安装"和"已审计"拆成了两个关卡:前者交给 pip,后者交给 pip-audit。
这篇不碰真实仓库,也不读取私有包索引。我们只用一个公开的 Flask 固定版本做最小实验:完成漏洞扫描、读懂退出码、导出 JSON、生成脱敏 HTML,再把只读报告短时交给同事验收。
本文实测环境:Python 3.14.3、pip-audit 2.10.1。漏洞库会持续更新,因此复现时应以当次扫描生成的 JSON 为准,不要把截图中的漏洞数量写进长期判断逻辑。
1 什么是 pip-audit,它能检查什么?
pip-audit 是 Python Packaging Authority(PyPA)维护的 Python 依赖审计工具。它会检查 Python 环境、requirements 文件或受支持的项目文件,并把包版本与漏洞服务中的已知漏洞记录比对。默认漏洞服务是 PyPI,也支持 OSV 等服务。
这篇里它只负责三件事:读取公开示例依赖、输出机器可读 JSON、在升级后重新审计。它不是恶意代码查杀器,也不会证明业务代码绝对安全;"没有发现已知漏洞"的准确含义,是当前依赖版本在本次使用的漏洞数据源中没有匹配记录。
官方当前列出的输出格式包括 columns、json、markdown、cyclonedx-json 和 cyclonedx-xml。给人临时查看时,Markdown 很省事;需要二次加工、接 CI 或做脱敏报告时,JSON 更合适。
2 环境准备:只创建公开的最小测试项目
2.1 创建隔离目录和虚拟环境
本文命令在 macOS、Linux 的 Bash/Zsh 环境执行。Windows 用户可在 PowerShell 中创建虚拟环境后,把激活命令改为 .venv\Scripts\Activate.ps1。
bash
mkdir pip-audit-demo
cd pip-audit-demo
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pip-audit
pip-audit --version
pip-audit 2.10.1 官方要求 Python 3.10 及以上。如果安装阶段直接提示 Python 版本不兼容,先看 python --version,别急着改依赖文件。
这里把审计工具装进独立虚拟环境,是为了不污染系统 Python。更重要的是,后面始终明确传入 -r 指定的示例文件,不会误扫当前机器里其他项目的环境。
2.2 准备存在已知漏洞的公开样例
新建 requirements-vulnerable.txt:
txt
Flask==0.5
这是公开 PyPI 包的历史版本,只用于演示,不能拿去启动真实服务。本文不使用公司锁文件、不配置 --index-url 或 --extra-index-url,也不把私有包名写进任何报告。
3 扫描 requirements.txt,并正确处理退出码
3.1 先看终端表格结果
执行下面的审计命令:
bash
pip-audit \
--no-deps \
--requirement requirements-vulnerable.txt \
--progress-spinner off
printf 'exit_code=%s\n' "$?"
几个参数别混:
-r/--requirement:审计指定的 requirements 文件,可重复传入;--no-deps:要求输入中的直接依赖固定到精确版本,并跳过依赖解析;--progress-spinner off:关闭进度动画,保存日志时更干净;-f/--format:选择输出格式,默认是终端表格。
--no-deps 适合这个只有一个精确固定版本的教学输入。pip-audit 会提示更推荐带哈希的完整锁定依赖;正式项目应使用构建工具生成完整、带哈希的 requirements,再用 --require-hashes 审计,而不是手工补一串哈希。
本次在 2026-09-05 实测,Flask 0.5 命中 4 条已知漏洞记录,进程退出码为 1。表格中的 ID 是漏洞编号,Fix Versions 是漏洞数据源给出的修复版本集合。修复时要选择同时满足项目兼容性和全部漏洞约束的版本,不能只看到第一行就升级到第一个数字。
pip-audit 官方定义的当前退出码很直接:
0:没有检测到已知漏洞;1:发现一个或多个已知漏洞。
划重点:退出码 1 不是"命令坏了",而是审计关卡明确失败。CI 里应保留这个失败信号,不要常态化追加 || true。如果脚本需要先保存结果再决定流程,可以这样写:
bash
set +e
pip-audit --no-deps -r requirements-vulnerable.txt --progress-spinner off
AUDIT_RC=$?
set -e
if [ "$AUDIT_RC" -eq 1 ]; then
echo "发现已知漏洞,停止发布"
exit 1
elif [ "$AUDIT_RC" -ne 0 ]; then
echo "审计执行异常,退出码:$AUDIT_RC"
exit "$AUDIT_RC"
fi
这里没有把非 0 全都粗暴归为"有漏洞"。网络中断、输入错误或内部异常要按执行失败排查,不能伪装成一份可信的漏洞结论。
3.2 导出 JSON,而不是解析终端表格

报告转换必须消费 JSON,别用正则去抠对齐后的列文本:
bash
set +e
pip-audit \
--no-deps \
-r requirements-vulnerable.txt \
--progress-spinner off \
--format json \
--output audit.json
AUDIT_RC=$?
set -e
python -m json.tool audit.json >/dev/null
printf 'audit_exit_code=%s\n' "$AUDIT_RC"
--output audit.json 把结构化结果写入文件;发现漏洞时,文件仍会生成,同时命令保持退出码 1。python -m json.tool 是一个很实用的小检查:它不负责审计,只验证 JSON 能否正常解析。
2.10.1 的 JSON 顶层包含 dependencies 和 fixes。每个依赖对象包含名称、版本与 vulns 数组;每条漏洞记录包含 id、fix_versions、aliases 和 description。真正适合自动化判断的是这些字段,而不是截图里的行数。
4 把 JSON 转成脱敏、只读的 HTML 报告
原始 JSON 包含包名、版本、漏洞描述等交付信息,不该未经检查就对外共享。下面的转换脚本只提取依赖名、版本、漏洞编号和修复版本,不写用户名、本机路径、源码、索引地址或环境变量。
保存为 render_report.py:
python
import html
import json
from pathlib import Path
SOURCE = Path("audit.json")
OUTPUT_DIR = Path("report-public")
OUTPUT = OUTPUT_DIR / "index.html"
data = json.loads(SOURCE.read_text(encoding="utf-8"))
rows = []
for dependency in data.get("dependencies", []):
name = html.escape(str(dependency.get("name", "")))
version = html.escape(str(dependency.get("version", "")))
for vuln in dependency.get("vulns", []):
vuln_id = html.escape(str(vuln.get("id", "")))
fixes = ", ".join(map(str, vuln.get("fix_versions", []))) or "无已知修复版本"
rows.append(
"<tr>"
f"<td>{name}</td><td>{version}</td><td>{vuln_id}</td>"
f"<td>{html.escape(fixes)}</td>"
"</tr>"
)
if rows:
result = "".join(rows)
summary = f"发现 {len(rows)} 条已知漏洞记录"
else:
result = '<tr><td colspan="4">未发现已知漏洞</td></tr>'
summary = "未发现已知漏洞"
page = f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Python 依赖审计摘要</title>
<style>
body{{font:16px/1.6 system-ui;max-width:960px;margin:40px auto;padding:0 20px;color:#1f2937}}
table{{width:100%;border-collapse:collapse}}
th,td{{border:1px solid #d1d5db;padding:10px;text-align:left}}
th{{background:#f3f4f6}} .note{{background:#fff7ed;padding:12px}}
</style>
</head>
<body>
<h1>Python 依赖审计摘要</h1>
<p><strong>{html.escape(summary)}</strong></p>
<p class="note">本页仅含公开示例依赖的脱敏结果,不含源码、路径、凭据和私有索引。</p>
<table>
<thead><tr><th>依赖</th><th>版本</th><th>漏洞 ID</th><th>修复版本</th></tr></thead>
<tbody>{result}</tbody>
</table>
</body>
</html>
"""
OUTPUT_DIR.mkdir(exist_ok=True)
OUTPUT.write_text(page, encoding="utf-8")
print(f"generated: {OUTPUT}")
运行并检查报告:
bash
python render_report.py
python -m http.server 8000 --bind 127.0.0.1 --directory report-public
浏览器打开 http://127.0.0.1:8000/。这里必须看到摘要表格,而且地址只能在本机访问。若出现 404,优先检查当前目录下是否真的存在 report-public/index.html;若端口被占用,把命令和后面的 cpolar 端口一起改成同一个空闲端口。
这个目录只放生成后的 index.html。不要在项目根目录启动 HTTP 服务,否则 .git、requirements、脚本和其他文件都会落入可请求范围。
5 升级依赖并做修复前后复检
根据本次扫描结果,Flask 0.5 的全部命中项都可由 Flask 3.1.3 覆盖。新建 requirements-fixed.txt:
txt
Flask==3.1.3
这里采用手工修改,是为了把依赖变更留在代码评审里。pip-audit 也提供 --fix 自动升级,但依赖大版本变化需要跑业务测试,不该把"自动升级成功"直接等同于"应用兼容"。
执行复检并重新生成报告:
bash
pip-audit \
--no-deps \
-r requirements-fixed.txt \
--progress-spinner off \
--format json \
--output audit.json
printf 'exit_code=%s\n' "$?"
python render_report.py
本文实测返回 No known vulnerabilities found,退出码为 0;生成的 JSON 中各依赖的 vulns 均为空数组,HTML 摘要也显示未发现已知漏洞。别只盯着退出码,还要运行项目自己的单元测试、接口测试与兼容性检查,因为依赖审计不会验证业务行为。
另一个容易踩的坑是复检旧文件。升级 requirements 后必须重新执行 pip-audit 并覆盖 audit.json,再生成 HTML;只刷新浏览器不会更新扫描结果。
6 用 cpolar 短时分享脱敏报告

同事不在当前局域网时,可以临时映射这个静态报告。前提是本地服务仍严格绑定 127.0.0.1,分享目录中只有脱敏后的只读 HTML。
cpolar 请从官网下载页安装并完成账号绑定:
- 官网下载:download下载 - cpolar 极点云官网
- 官方文档:文档 - cpolar 极点云官网
保持本地静态服务运行,再开另一个终端执行:
bash
cpolar http 8000
命令启动后,从 cpolar 输出或本地 Web UI 的在线隧道列表中读取本次真实公网地址,再把该地址发给指定验收人。随机公网地址不是访问控制;任何拿到链接的人都能请求报告,所以这里只适合短时间、低敏、已脱敏的验收材料。
这条边界我建议写进团队检查项:
- 不共享
audit.json原文件,只共享字段收敛后的 HTML; - 不在报告中放用户名、绝对路径、仓库地址、私有包名、Token 或私有索引;
- 不映射源码目录、虚拟环境、包仓库、管理后台和命令执行接口;
- 不把临时地址贴进公开群、工单或长期文档;
- 验收结束立刻关闭隧道和本地 HTTP 服务。
关闭时,在运行 cpolar http 8000 的终端按 Ctrl+C,再到本地 Web UI http://127.0.0.1:9200 确认该隧道已不在线;随后回到静态服务终端按一次 Ctrl+C。浏览器再次访问本地 8000 端口应连接失败,这才算真正收口。
7 常见问题:结果不对时先查哪里
7.1 为什么"装得上"仍然返回 1?
安装成功只代表包能被解析和下载。pip-audit 返回 1 代表漏洞服务已为某个包版本记录了已知漏洞,两者判断的是不同问题。
7.2 为什么同一示例的漏洞数量变了?
漏洞数据库会新增、合并或更新记录。自动化流程应判断退出码和 JSON 的 vulns 数组,不应断言永远固定为某个数量。文章中的 4 条是 2026-09-05 的实测快照。
7.3 为什么用了 --no-deps 仍收到警告?
这是工具在提醒:精确固定版本不等于完整性保护。正式交付更推荐完整锁定并携带哈希,再用 --require-hashes 检查。不要为了让日志"好看"而隐藏这个提醒。
7.4 网络失败后还能相信旧 audit.json 吗?
不能。命令异常退出时,旧文件只是上一次扫描结果。流水线应给每次任务使用新的输出目录,或在扫描前删除旧产物,并且只有本次命令按预期完成后才发布报告。
7.5 能否直接忽略某个漏洞?
pip-audit 提供可重复使用的 --ignore-vuln ID,也接受该漏洞的别名。但忽略不是修复。只有团队确认漏洞不适用于当前使用方式,并留下风险说明、负责人和复审期限时,才应把忽略规则纳入配置。
8 总结
现在这条链路已经完整跑通:公开示例 requirements 进入 pip-audit,漏洞结果落成 JSON,修复版本完成复检,必要时再生成字段收敛的静态 HTML,交给同事做一次短时验收。
- 把
pip install与pip-audit分成两个发布关卡,退出码1要被认真处理; - 报告转换只读 JSON 字段,不解析终端表格,也不带出路径、凭据和私有依赖;
- cpolar 只映射绑定在
127.0.0.1的脱敏静态页,验收结束同时关闭隧道和 HTTP 服务。
后续可以把审计命令放进 CI,并保留 JSON 或 CycloneDX 产物供内部追踪。真正省事的做法不是长期公开一张报告,而是让每次依赖变更都自动留下可复查、可阻断、不过度暴露信息的审计结果。
参考资料
- pip-audit 官方仓库与使用说明:https://github.com/pypa/pip-audit
- pip-audit PyPI 项目页:pip-audit · PyPI
- Python Packaging Advisory Database:https://github.com/pypa/advisory-database
- cpolar 官方下载页:download下载 - cpolar 极点云官网
- cpolar 官方文档:文档 - cpolar 极点云官网