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

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、在升级后重新审计。它不是恶意代码查杀器,也不会证明业务代码绝对安全;"没有发现已知漏洞"的准确含义,是当前依赖版本在本次使用的漏洞数据源中没有匹配记录。

官方当前列出的输出格式包括 columnsjsonmarkdowncyclonedx-jsoncyclonedx-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 把结构化结果写入文件;发现漏洞时,文件仍会生成,同时命令保持退出码 1python -m json.tool 是一个很实用的小检查:它不负责审计,只验证 JSON 能否正常解析。

2.10.1 的 JSON 顶层包含 dependenciesfixes。每个依赖对象包含名称、版本与 vulns 数组;每条漏洞记录包含 idfix_versionsaliasesdescription。真正适合自动化判断的是这些字段,而不是截图里的行数。

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 请从官网下载页安装并完成账号绑定:

保持本地静态服务运行,再开另一个终端执行:

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 installpip-audit 分成两个发布关卡,退出码 1 要被认真处理;
  • 报告转换只读 JSON 字段,不解析终端表格,也不带出路径、凭据和私有依赖;
  • cpolar 只映射绑定在 127.0.0.1 的脱敏静态页,验收结束同时关闭隧道和 HTTP 服务。

后续可以把审计命令放进 CI,并保留 JSON 或 CycloneDX 产物供内部追踪。真正省事的做法不是长期公开一张报告,而是让每次依赖变更都自动留下可复查、可阻断、不过度暴露信息的审计结果。

参考资料

相关推荐
测试19981 小时前
Jmeter接口自动化生成测试报告html格式详解
自动化测试·python·测试工具·jmeter·职场和发展·测试用例·接口测试
pjj198541 小时前
Hadoop-python中使用大数据
大数据·hadoop·python
小刘在重生~2 小时前
Django|Excel 批量上传、Form/ModelForm 文件上传、Media 媒体文件配置
python·django·excel
威联通安全存储2 小时前
TS-h1887XU-RP 在模具制造行业试模与改模数据场景的部署
python·制造
步行cgn2 小时前
Spring Environment 详解:Spring Boot 配置管理的核心接口
spring boot·python·spring
Csvn2 小时前
🐍 Day 12: 编码与字符集 — 告别乱码噩梦
后端·python
2601_962283882 小时前
Python 开发框架:Django、Flask和FastAPI
python·django·flask·fastapi·web开发
lzfshub2 小时前
Open-DIS Python发送DIS实体状态PDU:实现坦克炮塔与主炮部件参数
java·网络·python·dis