PyPI 每周新增包数量在 2025 年陡然攀升了 30%,曲线近乎指数级。开发者 Artem Golubin 的监测数据把这条曲线拉出来后,安全社区立刻警觉:这不是生态繁荣,更像是一场由 AI 编码工具驱动的"包海啸"。大量低质量、重复甚至恶意伪装的包正以机器速度涌入 Python 的核心仓库,而你的 pip install 可能正在不知不觉中把它们拉进生产环境。
增长的不是质量,是数量
30% 的周增长率放在任何开源生态里都算惊人,但拆开看就会发现端倪。传统模式下,一个开发者发布包需要写文档、测试、设计 API——这些步骤天然限制了发布频率。而 AI 辅助编码把这道门槛几乎削平:生成代码、补全 README、自动打包上传,几分钟就能走完整个流程。
问题不在于 AI 写代码本身,而在于发布成本趋近于零后,筛选成本全部转移到了消费端。PyPI 的审核机制历来是轻量级的,它依赖社区反馈和事后清理,而非发布前的质量把关。当包的增速远超人工审查的处理能力,垃圾包和恶意包就有了充足的藏身空间。
三类风险正在浮出水面
1. 同名/近名混淆攻击(Typosquatting)
AI 让批量生成变体包变得极其廉价。一个热门包叫 requests,攻击者可以瞬间注册 reqeests、requestz、requests-plus 等数十个变体,每个都配上 AI 生成的看起来合理的 README 和示例代码。用户一次手误,就可能装进恶意包。
2. 依赖混淆(Dependency Confusion)
内部私有包的名字被人在 PyPI 上抢先注册同名公开包,版本号设得更高。pip install 默认优先拉取公开仓库的更高版本,私有包就被悄悄替换了。AI 降低了侦察和构造这类包的门槛——扫描企业 GitHub 仓库的 requirements.txt,批量生成同名包,完全可以自动化。
3. 功能空洞的僵尸包
这类包不恶意,但毫无维护价值。它们是 AI 编码实验的副产品——开发者让模型生成一个"工具库",发布后不再维护。这些包占着命名空间、污染搜索结果、增加依赖树的冗余度,最终拖慢整个生态的信任筛选效率。
上手实践:给你的依赖链加一道安检
下面是一个可以直接运行的 Python 脚本,用来扫描你当前项目依赖中可能存在风险的包。它检查三个维度:包龄过短(小于 30 天)、维护者数量为 1、版本号异常偏高。这些特征在 AI 批量生成的包中高频出现。
"""
dep_guard.py — 扫描项目依赖中的高风险包
用法: python dep_guard.py requirements.txt
依赖: pip install requests
"""
import sys
import json
from datetime import datetime, timedelta
import requests
PYPI_API = "https://pypi.org/pypi/{name}/json"
RISK_THRESHOLD_DAYS = 30 # 包龄低于此值视为高风险
def check_package(name: str) -> dict:
"""从 PyPI API 拉取包元数据,返回风险评估结果"""
resp = requests.get(PYPI_API.format(name=name), timeout=10)
if resp.status_code != 200:
return {"name": name, "status": "not_found_on_pypi", "risk": "high"}
data = resp.json()
info = data.get("info", {})
releases = data.get("releases", {})
# 计算包龄:取最早版本的发布时间
first_release_date = None
for ver, files in releases.items():
for f in files:
dt = datetime.fromisoformat(f["upload_time_iso_8601"].replace("Z", "+00:00"))
if first_release_date is None or dt < first_release_date:
first_release_date = dt
age_days = (datetime.now(first_release_date.tzinfo) - first_release_date).days if first_release_date else -1
# 维护者数量(PyPI JSON API 不直接返回,用 author 字段粗估)
author_raw = info.get("author") or ""
maintainer_raw = info.get("maintainer") or ""
people = set()
for field in [author_raw, maintainer_raw]:
if field:
# 简单按逗号/分号拆分
for p in field.replace(";", ",").split(","):
p = p.strip()
if p:
people.add(p)
# 最新版本号
latest_ver = info.get("version", "0.0.0")
# 风险判定
risks = []
if 0 < age_days < RISK_THRESHOLD_DAYS:
risks.append(f"包龄仅 {age_days} 天")
if len(people) <= 1:
risks.append("维护者仅 1 人")
# 版本号异常高(如刚发布就是 99.0.0)——依赖混淆常见手法
try:
major = int(latest_ver.split(".")[0])
if major >= 50 and age_days < 365:
risks.append(f"版本号 {latest_ver} 异常偏高")
except ValueError:
pass
return {
"name": name,
"age_days": age_days,
"maintainers": len(people),
"latest_version": latest_ver,
"risk": "high" if risks else "low",
"risk_details": risks,
}
def main():
if len(sys.argv) < 2:
print("用法: python dep_guard.py requirements.txt")
sys.exit(1)
with open(sys.argv[1]) as f:
names = [line.strip().split("==")[0].split(">=")[0].split("<=")[0].split("~=")[0]
for line in f if line.strip() and not line.startswith("#")]
print(f"扫描 {len(names)} 个依赖包...\n")
flagged = []
for name in names:
result = check_package(name)
status_icon = "⚠️" if result["risk"] == "high" else "✅"
print(f"{status_icon} {result['name']} 龄={result['age_days']}天 维护者={result['maintainers']} 版本={result['latest_version']}")
if result["risk"] == "high":
for detail in result["risk_details"]:
print(f" → {detail}")
flagged.append(result)
print(f"\n高风险包: {len(flagged)}/{len(names)}")
if flagged:
print("建议逐一审查,确认是否为必要依赖。")
if __name__ == "__main__":
main()
运行方式:
# 先安装 requests(脚本自身依赖)
pip install requests
# 扫描你的 requirements.txt
python dep_guard.py requirements.txt
输出会标记每个包的风险等级。包龄低于 30 天、维护者仅一人、版本号异常偏高——这三项中命中任意一项就标 ⚠️。这不是最终判定,但足以帮你缩小审查范围。
安装时的即时防护
除了事后扫描,安装环节也能加一道过滤:
# 只安装包龄超过 90 天的包(需要 pip 21.1+)
pip install --no-deps some-package # 先不拉依赖,手动审查后再装
# 用 pip-audit 检查已知漏洞
pip install pip-audit
pip-audit # 扫描当前环境所有依赖的已知 CVE
pip-audit 走的是 PyPI 官方漏洞数据库,能抓到已上报的恶意包和 CVE,但对尚未被举报的新垃圾包无效——这正是 AI 海啸下最危险的灰色地带。
面对包海啸,开发团队该做什么
这场增长不会逆转。AI 编码工具只会更普及、更自动化,PyPI 的发布量大概率继续攀升。与其指望平台侧加严审核(这和 PyPI 的开放定位冲突),不如把防线建在自己这一端:
- 锁定依赖版本:
requirements.txt或pyproject.toml里写死版本号和 hash,不用模糊范围(>=、~=)。 - 引入私有镜像:用 Artifactory 或 devpi 建内部镜像,只同步你审查过的包,阻断依赖混淆攻击路径。
- 新包准入流程:团队新增任何依赖前,跑一遍
dep_guard.py或类似工具,包龄不足 60 天的必须人工审查源码。 - 定期清理:每季度跑
pip-audit+ 依赖树分析,剔除不再使用的包,减少暴露面。
PyPI 仍然是 Python 生态的基石,但信任模型正在被 AI 批量生产逼到极限。当发布成本趋近于零,唯一有效的应对是把筛选成本前置到安装之前——不是等平台替你挡,而是自己先看一眼再拉进来。