2025年9月8日,一位知名维护者 @qix 的 npm 登录账号被钓鱼。几分钟之内,18个包的投毒版本就发了出去:debug、chalk、ansi-styles、strip-ansi……这些包每周合计约20亿次下载,深埋在数以百万计的应用里。
![]()
第一份公开安全公告在74分钟后才出现在 OSV 上。在那之前,所有依赖公告库工作的扫描器给出的结论都一样:一切正常。
这些扫描器没坏。它们只是在回答一个错误的问题。
没人问的那个问题
安全工具问的是"这个版本是否已知有问题"。这套逻辑只有在有人发现、调查并发布公告之后才成立。但9月8日的危险不是一个包,而是一个人的账号。
更该问的是:谁能把代码推进我的应用,每个人又能触及多少?公告数据库回答不了这个问题,因为答案不是某个包的事实,而是依赖关系的形状——你、你的90个直接依赖、上千个隐藏依赖,以及背后数百个能发布它们的人。这是个图问题。
图里长什么样
Chokepoint 读取 package-lock.json,向 npm 查询每个包的维护者,向 OSV 查询公告,然后为每个项目建一张 FalkorDB 图。项目依赖版本,版本依赖版本,版本属于包,维护者维护包、发布版本,公告影响版本。
回放用的是 webpack-dev-server 在2025年8月25日、即攻击前两周的真实 lockfile:1423个版本、90个直接依赖、685个 npm 账号、3108条关系。有一条从第一天就定下的规则:维护者只按 npm 用户名存储,邮箱读取后立即丢弃。
核心查询是:对每个 npm 账号,有多少个直接依赖最终会拉进它能发布的包?在回放项目上,@qix 覆盖了90个直接依赖中的43个。一次钓鱼登录,能触及近半个应用。
排序用 betweenness 打破平局(坐在多条路径必经之桥上的账号),再用 pageRank 衡量其控制版本的影响力。
一个花掉一晚上的教训
第一版用了变长模式匹配,看起来优雅,然后超时了。变长匹配会走遍每一条可能路径,而真实依赖图里路径数量是指数级的——菱形结构到处都是。换成 BFS 后每个节点只访问一次,答案一样,但能返回结果。
反过来问"有多少东西依赖这个版本"时,结论相反:从每个节点出发做 BFS,不如反向单次遍历加去重计数,后者快约10倍。图计算就是不断测量。
Nightjar:知道谁能投毒只是一半
另一半是察觉有人真的动手了。Nightjar 是 Chokepoint 的监视代理,每分钟检查应用相关包的新发布,对每个发布跑一遍循环:观察、在图中调查、决策、行动、记忆。
决策是纯规则,不用 AI,因为安全判定应当可解释、可核查:3分是 WATCH,6分是 ALERT。
回放9月8日 npm 的真实发布时间:13:12:10,ansi-styles@6.2.2,WATCH;13:12:39,debug@4.4.2,WATCH;13:13:05,chalk@5.6.1,ALERT——落在应用6个版本范围内(+3),是 @qix 在54秒内的第三次发布(+3),账号覆盖90个依赖中的43个(+1),总分7。
第一份公告14:26才到。Nightjar 会提前73分钟发出警报,而且完全不知道恶意软件的存在,只凭图的形状和发布的节奏。
触发 ALERT 时它会行动:只针对被投毒的那个版本,在 package.json 的 overrides 里锁定 lockfile 原有的版本,可以在仓库上开修复 PR,并通过桌面、Slack 或 Discord 通知你。
记忆也放进图里
Nightjar 的记忆是另一张独立的 FalkorDB 图,每个项目一张。它持久:重建项目图不会碰记忆。它会变:未决警报每轮对照 OSV 复查,转为 CONFIRMED 或 CLEARED,每次转变都是一条新决策,历史得以保留。它会反哺下一次决策:某账号此前的警报会提高其下一次发布的分数。它还能共享:Nightjar 写入,第二个代理读取。
所有查询都是 tools.py 里的函数,同一个 MCP 服务器对外提供。网站的 Ask 框就是该服务器的 MCP 客户端:LLM 从列表里挑工具,调用走 MCP,图负责计算答案,LLM 只把结果翻译成人话,从不写 Cypher。没有 LLM 也行,关键词规则会挑工具。Chokepoint 里没有任何东西必须依赖 AI。
回放与真机
网站是9月8日在真实数据上的回放:真实的发布时间、真实的公告时间、真实的 lockfile。真机版跑在你自己的机器上,用嵌入式 FalkorDB,不需要 Docker、不需要账号、不需要服务器,数据不出笔记本。接入 LLM 或 GitHub token,它们也留在你机器上。
@qix 没有做错任何事:账号被钓鱼,换成谁都可能中招。重点不是追责,而是在事情发生之前就知道,你的应用有多少部分悬在一个登录账号后面。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.