2026 年 10 月 21 日到 22 日这两天,安全社区接连处理了四件事,发布时间几乎重叠在一起。
第一件:PHP 项目在 TechCrunch 的报道里紧急发布补丁,修复两个严重级别的缺陷(CVE-2026-109618 与 CVE-2026-109619,合称 PHP2Shell)。美国国家安全局发出少见级别的公开提醒:来不及打补丁的站点,应该先下线。法国部分省级行政网站、德国数字事务部门、荷兰议会与英国预算责任办公室等一批公共服务站点,采取了先下线再打补丁的做法。
第二件:广为使用的 LLM 路由层 LiteLLM 当天发布到 PyPI 的 1.107.0 版本携带了凭据窃取器与远程代码加载器,恶意载荷只存在于预编译 wheel 里,源码发布包中看不到。
第三件:Node.js 同日修复了四个高危问题,其中一个允许远程执行代码,两个允许绕过 TLS 校验。
第四件:Railway 在次日要求所有用户轮换 API 与云密钥,因为加密后的环境变量有部分被读取并解密。
我在做本次夜间采集时把这四件事放进同一个窗口里看,起因是想弄清依赖漏洞通常是怎么进到我这边的,发现它们指向的不是同一个主题,而是同一件事的两头:补丁到达服务器的速度,与恶意代码到达服务器的速度,都不再由人决定,而这两条时间的尺度差得很远。 前半句是 PHP2Shell,后半句是 LiteLLM。中间被挤掉的那一层,正是依赖漏洞进入 AI 编码链路的路径,也是本文要拆的部分。
这个缺陷真正值得注意的是它的时间跨度,而不是它的危害评分。
按 国内媒体的转述与多家英文报道,PHP 基金会的研究人员在 2024 年就已经识别出一个理论层面的缺陷,但长期没能写出可用的利用路径。2026 年 7 月,基金会加入 OpenAI 的「Patch the Planet」计划;8 月 11 日,参与该计划的安全公司 Calif 用 GPT-5.6 Sol 报告了第一枚缺陷;8 月 24 日,Wiz 用自家的 Atlas 安全智能体与 Gemini 4 Argon 等模型报告了第二枚。两边都走了负责任披露的流程。
真正推着时间点往前跑的是两次意外:一次是基础细节在 10 月 20 日提前流到社交媒体,一次是 Calif 误发布了一段演示如何利用的录像并在黑客论坛扩散。基金会因此把修复提前放出。
Wiz 在自己的分析里给了一句判断,我认为是这一整周里最值得读的一句话:过去这类内存缺陷之所以没有被大规模利用,是因为针对不同 PHP 版本、不同配置、不同内存布局去适配一个利用,成本高到不划算;而今天模型帮着做这份适配。编辑谨慎起见,我把这句话当作厂商自述的一家口径,不构成结论。
同一家云服务商负责人的口径则是另一个方向:披露之后,其网络上出现了数以百万计的 PHP2Shell 利用尝试。这两个口径看起来打架,其实不冲突——一个说的是披露之前没有观察到在野利用,一个说的是披露之后尝试数量暴涨。我们在正文里同时保留,并按各自来源标注。
修复版本为 8.5.12、8.4.27、8.3.36 与 8.2.35;PHP 8.1 及更早版本已不在官方支持范围内。受影响范围的口径也要注意:英文报道有的写「自 2004 年发布的 PHP 5 起存在」,有的写「可追溯到 2012 年发布的 PHP 5.4」,两种说法并存,本次未核到 PHP 官方原文,故不作为结论使用。
| 时间 | 事件 | 来源口径 |
|---|---|---|
| 2024 年 | PHP 基金会识别理论缺陷,未能构造利用 | 行业报道一致提及,未见一手公告 |
| 2026-07 | 基金会加入 Patch the Planet 计划 | OpenAI 披露计划,多家转述 |
| 2026-08-11 | Calif 用 GPT-5.6 Sol 报告第一枚缺陷 | OpenAI 披露 |
| 2026-08-24 | Wiz 报告第二枚缺陷 | Wiz 与 Google 系模型并行使用 |
| 2026-10-20 | 基础细节泄露,加上一段演示录像扩散 | PHP 基金会叙述,多家转述 |
| 2026-10-21 | 紧急补丁发布,NSA 发公开提醒 | TechCrunch 与多家媒体 |
| 2026-10-21 之后 | 披露后利用尝试数量快速上升 | 云服务商负责人自述,未验证 |
| 持续中 | 部分公共服务站点先下线再补丁 | 多家公开名单,口径不一 |
按各家公布的时间点估算,恶意版的 litellm 1.107.0 在 UTC 11:39:51 进入 PyPI,13:23 被隔离,存活约 1 小时 43 分钟;对应的容器镜像从 11:46 提供服务到 14:20 被移除。这个数字是按两个公开时间点推算的,不是任何一家直接给出的结论,属于本次的计算结果,供参考。
真正让它值得和 PHP2Shell 放在一起看的是两点。
第一点,恶意载荷只在预编译 wheel 里。同一个版本的源码发布包里只有一行多余的 data = "cook",没有载荷文件。也就是说,审可读的源码、看 Git 标签上的签名提交,都看不出问题;真正造成伤害的是 wheel 里被塞进 site-packages 的三个文件:cook.pth、cook.py、sitecustomize.py。.pth 文件里的 import 语句在每次 Python 启动时执行,因此这台机器上任何 Python 进程——包括包管理器本身、IDE 语言服务、与 LiteLLM 毫无关系的脚本——都会触发窃取逻辑。
第二点,窃取清单里专门点名了 AI 编码工具。除 SSH 与 GPG 密钥、云凭据、浏览器保存密码、shell 历史、CI 与容器密钥之外,它明确去找 Claude Code、Claude Desktop、Cursor、Windsurf、Copilot、VS Code 的 MCP 配置文件、Gemini CLI 与 Codex 的认证文件,也去找 LiteLLM 代理自己数据库里存放的供应商 API key。这是一份把 AI 工具自身的凭据当作战利品的清单。
Wiz 方面观察到的后续行为尤其值得一提:数据离开受害者系统约 4.5 小时后,攻击者开始验证从 CI 运行器与 Kubernetes 节点取得的云凭据,并检查能否触达托管大模型服务。这条路径已经在工程圈里有了名字,叫 LLMjacking——偷来的不是钱,是算力账单。
LiteLLM 自己的事件报告给出的链条是这样的:攻击者持有三名工程师的命令行工具的 OAuth token,其中一个账户修改了发布流水线并触发发布,第二个账户通过了审批门禁,第三个账户删除了工作流记录,导致可用于回溯的日志变少;构建记录里显示的源仓库名字与合法仓库只差一个连字符。
而 token 是从哪里来的?LiteLLM 自述在一个本应只对内的 LiteLLM 部署上发现了一个不起眼的账号,该部署暴露在公网。公司的判断是 token 来自这个部署的日志——他们观察到某些编码工具会把用户的 OAuth token 放进响应体,而响应体一旦被记录,token 就进了日志。同时公司也声明:尚未确认攻击者进入该部署的具体路径,目前怀疑是一个最近刚被打过补丁的问题,调查仍在进行。这一段全部是 LiteLLM 单方面的自述,我们未能核到一手证据,因此按「据自述」的口径写入,不作为结论。
| 检查项 | 位置 | 命中后怎么做 |
|---|---|---|
| 版本是否为 1.107.0 | pip show 结果或锁文件 | 立即隔离该主机,再清理 |
| 三个可疑文件 | site-packages 下的 cook.pth / cook.py / sitecustomize.py | 先移走,再启动 Python |
| 游离进程与锁文件 | 持有 远程代码加载 锁文件的 Python 进程 | 终止进程并检查外连 |
| 外接流量 | 到一个与 litellm 高度相似的域名 | 按完全失陷处理并轮换全部凭据 |
规模方面两个口径并存:一家供应链安全公司给出每周约 2300 万次下载,另一家给出过去一个月 PyPI 上超过一亿次下载与 2000 多个依赖它的包。两个数字统计口径不同,本次未做统一,都保留。
这两条时效差之间存在一个共同的中间层:无论关于依赖漏洞的情报更新得多快,最终动手改代码的常常是一个 Agent,而它的知识停在自己的训练截止点上。
2026 年 10 月 19 日提交的一篇论文做了一次大规模的实测。研究者用 37 个安全技能驱动 Claude Code,底层换四个不同模型,对 100 个真实内核缺陷、跨 10 个类别做了一遍,共 14,800 次运行。结果是:24.4% 的运行定位到正确的函数,18.3% 同时给出了函数与造成问题的那条语句,15.7% 正确判定了缺陷类别。作者把失败拆成六类:证据获取、状态关系推理、代码解读、候选验证、调查覆盖、报告。这项大规模实测工作是我这一周看到的、把「装了技能之后到底提高了多少」问得比较扎实的一次。它给出的一句提醒是:给 Agent 装上检查清单,不等于它就能把清单用对,这也是我在核对这批论文时反复对照的一点。
同期另一篇论文从表示空间解释了为什么会这样,这篇 arXiv 预印本的结论对做 AI 代码审查的团队尤其重要。研究者用 8 个 6.7B 到 15B 的代码模型,在 C 程序上做线性探针。结论是:模型能很好地编码「这段代码实例化了什么模式」,但对「这段代码是否有问题」的编码并不是一个全局可分的类别边界;这部分信息更容易以成对关系的形式被读出来——也就是「A 是否比 B 更危险」,而不是「A 是否有问题」。作者的推论是,模型能排序,但标准分类器在孤立样本上准确率有限,因为有缺陷与安全的样本在表示空间里分布重叠。
第三篇给了补救侧的一个数字:SPLASH 在 12 个模型针对 899 个任务生成的 63,684 个程序上,有 74% 的情况挑出了安全版本,在 58% 的不安全程序里把问题语句排在了第一位,表现优于一个前沿模型裁判。方法不依赖训练,也不依赖内部访问权限,原始论文给出了完整的 probing 设计与局限讨论。我们对这三组数字只做转述,没有复跑,因此只作为存在性证据。
模型读不到 2026 年 10 月的公告,但模型一定读 AGENTS.md 或 CLAUDE.md。于是近 14 天里出现了三件新东西,我按创建时间列出来,星数是 2026 年 10 月 22 日的快照:
三个项目都很小,星数也很低,値得注意的是它们的形态高度一致:都不是教模型去「多想想安全」,而是把一份外部时效情报放到模型的必读区。这条思路与 2026 年 10 月 20 日一篇关于软件供应链来源归属的论文方向接近——那篇论文检验的假设是,恶意包倾向于回避把自己回溯到一个源码仓库,而良性包倾向于把仓库链接挂出来。该论文顺着这条假设,检验了「去掉包注册中心、直接从源码构建」这条路线到底可不可行。
| 做法 | 补的是哪一段 | 代价与局限 |
|---|---|---|
| stalemap | 训练截止之后的依赖变更 | 需要维护一份白名单,覆盖面取决于输入源 |
| landmines | 当前锁定版本上的已知缺陷 | 依赖第三方缺陷库的时效,误报需人工判断 |
| appsecradar | Agent 已经写出来的成品 | 事后扫描,不能阻止引入;给出的是修复提示而非修复 |
第一部分是「有没有装进去」,也就是先把依赖漏洞这一类问题在你本机跑一遍,照着执行即可。
# 1. 确认本机版本 python -c "import litellm; print(litellm.__version__)" # 2. 确认可疑文件是否落盘 python -c "import site; print(site.getsitepackages())" # 3. 若确认为受影响版本:先隔离网络,再处理进程与文件,最后轮换凭据 # 安全版本线:1.106.2 / 1.107.0rc1 / 1.107.0rc2
第二部分是「怎么让模型知道它不知道的事」,也就是怎么把依赖漏洞的新进展推到它面前。下面这段脚本的思路与 stalemap、landmines 一致,只是把规则换成你自己维护的那份:把已知的问题版本写成一张表,每次 lock 之后生成一小段文字塞进 AGENTS.md。
# 生成一份「训练截止之后」的依赖简报,供 AGENTS.md / CLAUDE.md 引用 import json import subprocess import sys # 每条记录只写确切版本集合,不做区间比较: # 区间的开闭判断在不同锁文件格式里不一致,容易写错,宁可穷举 BAD_VERSIONS = { "litellm": {"1.107.0"}, } NOTES = { ("litellm", "1.107.0"): "预编译 wheel 携带窃取器,源码发布包中看不到;请改锁到 1.106.2", } def installed(): cmd = [sys.executable, "-m", "pip", "list", "--format=json"] out = subprocess.run(cmd, capture_output=True, text=True, check=True) return {p["name"].lower(): p["version"] for p in json.loads(out.stdout)} def brief(packages): lines = ["# auto-generated dependency brief", ""] for name, version in sorted(packages.items()): known = BAD_VERSIONS.get(name) if known and version in known: note = NOTES.get((name, version), "命中已知问题版本") lines.append("- " + name + " " + version + ":" + note) if len(lines) == 2: lines.append("- 当前未命中本地已知问题版本清单") return "\n".join(lines) if __name__ == "__main__": print(brief(installed()))
第三步是轮换范围。只要有一台机器跑过受影响版本,就按完全失陷来处理。我按各家给出的清单合并列出 LLM 供应商 API key、LiteLLM 代理库里的 key、云密钥与实例角色、SSH 与 GPG 密钥、浏览器保存密码、CI 与密钥库中的凭据、容器相关的凭据。这一步的顺序很重要:先隔离、再清理启动钩子、再轮换,反过来的话,新的凭据会经过同一条外发通道。
依赖漏洞和我日常用 AI 写代码有什么关系?
关系在两头。一头是你让 Agent 引入或修改的依赖,它不知道自己的训练截止之后发生了什么;另一头是 Agent 本身会读取一堆认证文件,而这些文件正是本轮窃取清单里的目标。两头都不是「AI 会不会作恶」的问题,而是依赖漏洞的时效与工具权限边界这两个工程问题。
PHP2Shell 这类依赖漏洞会影响到我吗?
取决于你维护的是不是服务端语言为 PHP 的站点。如果你的项目是 Python 或 TypeScript 生态,这个缺陷本身影响不到你;值得注意的是它的时间结构——一个埋了十几年的缺陷,在披露当天就可能面对数以百万计的尝试,这套时间结构决定了处置窗口被压得多短,它会在别的生态重演。
litellm 我没有直接安装,是不是就安全了?
不一定。公开分析提到有 2000 多个包依赖它,很多团队是间接引入的。查锁文件比查 requirements 更可靠,容器镜像则要比对镜像摘要,而不是只看标签。
升级到新版本就够了吗?
不够。受影响版本的窃取逻辑在 Python 启动时触发,一次运行就足以把凭据送出去。顺序应当是隔离、清理启动钩子、轮换凭据,升级排在最后。多家机构给出的处置建议都是这个顺序。
把依赖漏洞情报写进 AGENTS.md 真的有用吗?
有用,但要认清它的适用边界。它的作用是让 Agent 在动手之前读到一条它训练里没有的事实,能挡住的是「推荐一个已知有问题的版本」这类场景;挡不住「写出来的新代码里带了注入点」,这需要扫描与人工复核。两者互不替代。
这类低星项目值得试吗?
值不值取决于你有没有维护并列的那份已知问题清单。工具本身只是把它们搬到 Agent 的必读区,情报源才是成本所在。三款工具都很新,星数也是快照,建议先在个人项目里跑一周再决定是否进团队流程。
为什么文章里没有给出利用或复现方法?
这是本站的编辑口径:涉及在役攻击的部分一律只写检测与处置,不写可复现步骤。想做取证与红队演练的人,应该去看各家安全公司发布的完整 IoC 报告。