PyPI 最近改了一条规则:一个版本发布满 14 天后,就不能再往里面追加新文件了。听起来像个不起眼的运维调整,但它堵的是一条此前技术上一直走得通的路——如果某个项目的发布令牌或 CI 流程被攻破,攻击者理论上可以悄悄给一个用户已经用了多年、闭眼信任的老版本,追加一个带毒的文件。

PyPI 官方博客说得很直接:目前没发现这条路被真正用来攻击过,但技术上一直可行,只是没人证实动手过。这次是先把门焊死,不是等出事再堵漏洞。

新规到底改了什么

规则只针对一件事:给已发布超过 14 天的版本追加新文件。已上传的文件不会被删,普通用户下载安装老版本不受任何影响。

场景14天内14天后
追加新的 wheel / 签名文件允许拒绝
删除已上传文件一直不允许一直不允许
用户下载安装旧版本正常正常
想补文件怎么办直接补传只能发新版本号

真正受影响的,是那些习惯"先发布,过段时间再回来补 wheel"的维护者,以及依赖这个节奏的自动化流水线。两周期限一到,补传通道关闭,唯一的路是发一个新版本。建议做法很具体:检查自己项目的发布 CI 是不是分批构建多平台 wheel,如果是,趁现在改成一次性把所有平台的构建产物打齐再发布,或者干脆切到 trusted publishing 之类的自动化流程,减少人工延后补传的场景。

老版本为什么会被盯上,又为什么能被利用

这里要先分清两件事:PyPI 一直不允许覆写已发布文件的内容,那个口子从来没开过。新规堵的是另一种情况——给同一个版本号追加一个此前根本不存在的新文件,比如给一个只发过源码包的老版本,补上一个新的二进制 wheel。

这本来是正当功能,方便维护者事后补齐构建产物。但 pip 装包时有个习惯:同一版本号下,优先选 wheel,而不是源码包。如果攻击者拿到发布权限,给某个老版本追加一个专门为某个平台构建的恶意 wheel,很多用户下次安装这个"熟悉"的版本号时,拿到的就是新塞进去的毒文件,而不是原来那个用了多年、被审计过的源码包。

这也解释了为什么锁定版本号不够用。如果你的依赖只锁了版本号,没锁哈希,理论上还是可能被这种追加攻击命中。用了哈希锁定(比如 pip-compile --generate-hashes 生成的 requirements)的用户基本不受影响,因为哈希不匹配,pip 会直接拒绝安装。这也是给关注供应链安全的团队一个具体建议:核心依赖如果还没上哈希锁定,这是个该补的动作,不是因为这次的漏洞,而是因为这类风险本身一直存在。

我的判断

这条规则的成本很小,只是让少数维护者多跑一次发版流程。换到的收益是把一个此前无限期开放的攻击窗口,直接砍到 14 天。这种不对称买卖在安全治理里不常见,值得记一笔。

软件仓库这些年的走向都类似:npm、crates.io 这类生态早就在往"发布即定型"靠,PyPI 这次算是补上了自己这条线上的一个缺口。历史上电力、铁路这类基础设施的扩张,早期都靠事后追责,后来才慢慢转向前置约束——软件供应链现在走的也是这条老路。

不完全一样的地方是:14 天窗口内这条攻击路径依然存在,规则只是缩短了窗口,没有关掉它。攻击者是不是真用过这条路,目前还看不清,PyPI 自己也没说死。接下来该盯的,是别的包管理生态会不会跟进类似的时间限制,以及 PyPI 会不会进一步把"发布即不可变"扩展到强制要求一次性打齐所有平台 wheel。