ARTICLE / ai
审计的第二层:先校准尺子,再报漏洞
2026 年 11 月 4 日晚,v0.22.7 的增量审计收尾。报告最后一页是九行:Critical 0,High 3,Medium 2,Low 2,INFO 2。比上一版多了一行 High——不是因为世界突然危险了一点,而是一条原本记作 Medium 的发现被重新算成了 7.0,而 7.0 正好是 High 的下限。
真正让我停下来的是另外两个数字。同一个 connector 文件回传漏洞,上一版报告写 8.5,这一版按向量重算 7.1;同一个 execute_code 审批短路,上一版 7.5,这一版 7.8。代码在两版之间几乎没有动——六个高危模块一个字节都没改——动的是尺子。
这三处变化放在一起,构成了增量审计里最容易被跳过、也最不该跳过的一层。第一层是找到问题:读 diff、定位危险调用、写 PoC、拿终端输出。第二层是证明自己的判断站得住:分数从哪来、范围有没有漏、假设有没有被真实输出推翻。第一层决定报告里有没有东西,第二层决定那份东西半年后还能不能被别人接着用。
这一轮审计,第二层给出的东西比第一层多。九项发现里没有 Critical,最高的一条是 7.8;但尺子被校准了三次,其中一次是把自己原来的假设推翻了。后者不会出现在任何漏洞数据库里,却决定了下一轮审计是从零开始,还是从上一轮的结论继续往前走。
同一份代码,两版报告,三个分数
先说分数为什么会漂。CVSS 3.1 的基础分不是拍脑袋,它由八个向量决定:攻击向量、攻击复杂度、所需权限、用户交互、影响范围,加上机密性、完整性、可用性三个影响分量。前四个衡量「打进去有多难」,后三个衡量「打进去有多痛」;范围判断影响是否溢出到别的安全权威域。任何一个向量换一档,总分就会跳。
同一个漏洞,装在本地 CLI 里,攻击向量是 Local,所需权限可能是 Low;装成常驻网关、再加一个远程 web viewer,攻击向量就变成 Network,所需权限可能还是 Low——但可达的人从「坐在键盘前的人」变成了「能打开那个页面的人」。代码一行没改,分数该不该变?该变。这就是分数漂移里最容易被误判的一类:不是算错了,是部署形态变了,而报告没有把形态写进向量。
上一版给 connector 回传记 8.5,这一版重算 7.1。两个数字差 1.4,但真正的差别不在数值,在可复算性:这一版把向量串写进了报告,读者可以拿同一套向量重算一遍;上一版只留了结论。没有向量串的分数,换一个人、换一天、换一种心情,就会给出另一个数——这不是数学问题,是记录问题。
我的判断是:一个不可复算的分数,比一个偏低的分数更危险。偏低只是低估,不可复算会让整份报告的历史对比失效:下一轮看到 8.5 变成 7.1,无法判断是漏洞变轻了、部署变窄了,还是上一轮算错了。三种解释对应三种完全不同的处置,而报告没有给出区分它们的信息。
7.5 到 7.8:同一个触发面,两种算法
第二条分数变化更微妙。execute_code 整脚本审批对本地 viewer 回合短路,上一版记 7.5,这一版 7.8。代码逻辑没改,webapp 也不是这一版新增的——它在上一版就已经上线。差别在于这一版重算时,把「远程浏览器也是一个 viewer」单独计入了触发面;而上一版的 7.5 没有留下向量串,无法判断它有没有把远程主体算进去。同一个触发面,两种算法,差 0.3。
这不是「漏洞变严重」,是「同一件事被看得更完整」。远程浏览器接进来之后,用户交互不再需要本地操作,而是网络可达的交互;判定函数检查的是「会话的启动策略是不是本地类型」,不是「审批的人是不是坐在本地」。这个含义上的扩大在上一版就已经发生,只是分数到这一版才跟上。
这条变化说明:不能一刀切地说「分数变了就是尺子有问题」。要区分两类变化。第一类,代码或部署形态在这一版真的变了,分数应该变,报告要写清「因为什么变」。第二类,代码和形态都没变,分数却变了,那就是尺子的问题,必须解释——但「尺子的问题」不一定是算错,也可能是上一版看得不够完整。审计报告里最缺的往往不是分数本身,而是这个分类。
Medium 到 High:一次级别纠正,不是一次恶化
第三条最值得写。无人值守循环「首个即返回」、cron 的 deny 被吞,上一版记 Medium,附了一个星号的 7.0。这个星号在前一版里没有解释;按这一轮的复算,它对应的是「只在非默认配置下成立」。这一版按默认配置和攻击链重新算,7.0 落在 High 的下限,级别纠正为 High。
这句话的写法有讲究。报告里写的是「前版 Medium,本版 7.0 实为 High 下限——说明前版错了,不是本版变严重」。这两种表述对维护者的意义完全不同:前者要求他重新评估一条已知问题,后者会让他以为系统在一夜之间变危险了。级别纠正本身就是审阅价值,但前提是把「纠正」和「恶化」分开写。
这里还藏着一个更普遍的问题:CVSS 是配置的函数。同一个缺陷,默认配置下不可达,非默认配置下可达,分数该按哪个算?这一轮的做法是:主分数按默认配置,非默认配置单独标注,并在脚注里写清触发条件。这不是洁癖,是因为「脱离部署形态谈分数」和「脱离样本量谈显著性」是同一种错误——数字看起来精确,前提却是空的。
表 1 同一批发现在两版报告里的编号、分数与变化性质(快照:2026-11-04 晚;分数为本轮按 CVSS 3.1 自评,不是上游评级)
| 本轮编号 | 缺陷 | 上一版记录 | 本轮记录 | 变化性质 | 可复算依据 |
|---|---|---|---|---|---|
| V227-01 | connector 文件回传零 URL/host 校验 | 8.5 High | 7.1 High | 向量重算,部署形态写进报告 | 向量串 + PoC 终端输出 |
| V227-02 | execute_code 整脚本审批被本地 viewer 短路 | 7.5 High | 7.8 High | 重新评估:同一触发面按更完整的向量计算 | 差分 PoC 三组对照 |
| V227-04 | 无人值守首个即返回,cron deny 被吞 | 7.0* Medium | 7.0 High | 级别纠正:前版记错,非本版恶化 | 默认配置下的攻击链复算 |
| V227-03 | 八进制 IPv4 绕过 SSRF 判定 | 6.1 Medium | 6.1 Medium | 未变:加固仍未覆盖该写法 | 两条 URL 的双端解析对照 |
| V227-07 | 准入调用未传 submitter | 3.6 Low | 3.6 Low | 未变:当前不可直接利用 | 函数级取证 |
| V227-06 | Bitwarden 句柄尾部直入 argv | 2.5 Low | 2.5 Low | 未变:同类问题另一后端已修 | 参数列表取证 |
| V227-05 | 主渲染无 CSP + 宽 IPC 桥 | 未记录 | 4.2 Medium | 本版新增的纵深防御缺口 | 窗口配置与 preload 面抽查 |
表里最容易被误读的是 V227-02。分数从 7.5 升到 7.8,看起来像「问题变严重了」,实际是「同一扇门,这次量得更完整」。两者都需要修,但优先级判断不同:一个说明旧逻辑的触发条件比上一版记录得更宽,一个说明新功能把旧逻辑的隐含前提打破了。审计报告如果不写这一句,维护者只能看到一个更高的数字。
尺子为什么需要三个人签字
CVSS 的计算器不是难点,难点是向量选择背后的三个判断:攻击者从哪来、需不需要别人配合、影响落在谁身上。这三件事在审计里往往由一个人拍板,而一个人拍板最容易出现「同一个人两次给出不同答案」。
这一轮的做法是把三个角色拆开:读代码的人只负责指出「这条路径上缺了什么校验」,写 PoC 的人只负责证明「缺了之后能发生什么」,算分的人只看前两份材料、不看代码作者的解释。第三个角色最容易偷懒——他倾向于相信前两个人的结论,然后给一个「听起来合理」的分数。所以报告要求他必须写出向量串,并且对每个向量标注依据:攻击向量依据部署形态,所需权限依据触发前提,影响分量依据 PoC 实际读写了什么。
这一步的价值在 V227-04 上体现得最清楚。上一版把它记成 Medium;按这一轮的复算,那个评级只在默认配置下成立。这一版重新看攻击链:单次查询模式设为 approve、cron 模式设为 deny,两个标记同时存在时,函数在第一个上下文就返回批准,cron 的 deny 从来没有被读到。默认配置两者都是 deny,所以确实不可达——但「默认不可达」不等于「Medium」,它等于「按默认配置算是 Medium,按可达配置算是 High 7.0」。上一版把这两句话合并成了一句,于是丢掉了条件。
这一轮的处理是:主分数按默认配置给 7.0 并定级 High,非默认配置的可达条件单独标注。这样维护者能自己判断:如果你的部署改过这两个开关,它是 High;如果没改过,它是 Medium。分数没有变,可操作性变了。
行业里所有人都在补同一块短板
这不是一家的问题。11 月 3 日的生态日报里,「安全边界默认可信」被列为两个头部开源智能体项目的共同技术方向,具体诉求写得很直白:agent 不应能自己关掉自己的护栏,默认安全不可被悄然破坏;权限变更需要不可绕过的审计与用户确认路径。同一天的 CLI 日报把下一轮竞争点写成「权限决策的可审计性」,并记录了一条趋势:安全默认值正在从「拦截一切」转向「可解释 + 用户可覆盖」。
两条放在一起看,方向是一致的:行业已经不缺「有没有拦」这个答案,缺的是「为什么拦、拦了谁、能不能复核」。这正是审计报告的第二层要解决的问题——不是多报一个洞,而是让每一个判断都能被下一个人复核。
同日的工具动态还提供了一个技术上的旁证。Gemini CLI 的一个扩展下载加固 PR 同时做了四件事:流式错误处理、部分文件清理、SSRF 与重定向防护、socket 超时。四件事全在下载链路上——流断了怎么办、下了一半的残文件怎么清、跳转到内网算不算 SSRF、连接卡住多久算超时。这和我们这边 connector 文件回传的问题是同一类:只要一个 agent 能根据远端返回的内容发起第二次请求,下载链路就同时是功能面和攻击面。区别只在于,有的项目在加防护,有的项目连上一版的 PoC 都还没回归。
六个模块一个字节没动,这才是最该紧张的信号
增量审计的第一个动作是拉源码副本,然后对高危模块跑一次 git diff --stat。v0.22.6 到 v0.22.7,connectors、url_safety、path、skills、tirith、vault 六个模块的输出是空的——不是「改动很小」,是一个字节都没动。
空 diff 在审计里是一个双重信号。第一种读法是人人都有的直觉:没改,所以没问题。第二种读法正好相反:没改,所以上一版的结论原样继承——包括上一版发现的洞。第二种读法才是对的,因为漏洞不会因为无人触碰而消失,只会因为无人触碰而保持原样。
这一轮把六个模块的上一版 PoC 全部重跑了一遍。四项旧洞全部复现:connector 回传仍然可以把任意 URL 的内容写进本地下载目录;execute_code 的整脚本审批仍然会被持有审批能力的 viewer 短路;无人值守循环仍然在第一个上下文返回;八进制 IPv4 仍然能在校验端被读成公网地址、在连接端被读成回环。四项的 PoC 输出都带时间戳归档,报告里每一句结论都能指回一条终端记录。
表 2 六个高危模块的改动量与回归结果(空 diff = 零改动;PoC 输出为本轮重跑)
| 模块 | v0.22.6→v0.22.7 改动 | 上一版发现 | 本轮回归 | 版本状态 |
|---|---|---|---|---|
| connectors 文件回传 | 空 diff | V226-01,8.5 | 复现:任意 URL 落盘 | 连续第 5 版未修复 |
| execute_code 审批 | 空 diff | V226-02,7.5 | 复现:viewer 短路 | 连续 5 个 tag 未改 |
| 无人值守 deny 优先 | 空 diff | V226-04,7.0* | 复现:首个即返回 | 未修复 |
| URL 安全 / 八进制解析 | 空 diff | V226-03,6.1 | 复现:双端解析分叉 | 加固未覆盖 |
| skills 守卫 | 空 diff | 上版无高危 | 未新增发现 | 维持 |
| vault / 凭据后端 | 空 diff | V226-06,2.5 | 复现:Bitwarden 参数注入 | 同类另一后端已修 |
这张表真正想说的是:零改动模块的优先级,应该高于改动模块。改动模块至少有作者在看,零改动模块连「有人看过」都谈不上。审计预算有限的时候,先回归旧洞,再读新代码——因为旧洞已经被证明可达,新代码只是可能可达。
「连续第 5 版」是一句时序事实
V227-01 的报告里写着「连续第 5 版未修复」。这句话不是修辞。v0.22.0 第一次记录,v0.22.1、v0.22.2 两次复核都在;中间三个 tag 没有独立审计目录,靠版本 diff 与这个文件的零改动推定仍在;v0.22.6 是第四个版本,v0.22.7 是第五个。五个版本的时间跨度里,这个文件的 diff 始终为零。
为什么值得单独写一句?因为它把一个技术判断变成了一个可以被排期的事实。维护者每天收到大量报告,单条「这里缺一个校验」很容易被归入「以后再说」;「这个洞已经连续五个版本存在,且每一版都有可复现的 PoC」是一个有时序的陈述,它可以被写进 release notes、被放进下一个里程碑、被用来回答「为什么还没修」。
但这句话也有边界。连续未修不等于维护者无视,可能是不在优先级、可能是修复会破坏兼容、可能是披露流程还没走完。审计只能陈述版本状态,不能推断动机。报告里只写版本状态、不写维护者的动机,就是为了不让时序事实变成道德指控。
空输出是最危险的输出
比空 diff 更隐蔽的是扫描器的空输出。仓库里有一个按危险模式扫描的脚本,仍然按旧版本的行号签名匹配——approval.py:3875 那一套。而审批逻辑早已拆成模块化布局,行号全变了。对着 v0.22.7 跑,输出是空的或者错位的。
空输出会被读成「干净」。这是所有静态扫描工具的通病:它的沉默既可能是没有问题,也可能是它根本没在看。两者的输出完全一样,而人的默认解读总是前者。
这一轮的处理是把扫描器降级为「覆盖提示」:它报出来的东西要人工复核,它没报的东西不能当作结论。报告里必须写清 SKIP 的数量和漏报的范围,否则「扫描通过」会变成一个比实际强得多的断言。最终结论只认人工定位加 PoC 的真实输出。
这条限制看起来是工具问题,实际是方法问题:任何依赖行号的检测,在模块拆分之后都会静默失效,而失效的表现是「没有告警」。同样的道理适用于规则引擎——一条规则写在某个文件的某一行,文件一拆,规则还在跑,只是再也匹配不到东西。
旧洞的代价不是分数,是攻击面复利
这一版没有在 connector 或审批逻辑里加新代码,webapp 也是上一版加的。看起来什么都没变。上一版对它的记录是:登录 cookie 与浏览器凭据做 HMAC 绑定,token 轮换即全员登出,socket ticket 带凭据代次,CSP 用 hash 加 self,没有 unsafe-inline。单看那份记录,找不到明显错误。
问题在于它把一个新的主体接进了旧逻辑。远程浏览器也能成为 viewer,而 viewer 只要持有审批能力,就会命中那个 2026 年第三季度写下的短路条件。那个条件在被写下的那一天是合理的:本地 CLI、TUI、桌面回合,用户就坐在键盘前,弹窗给他看和自己点允许没有区别。当 viewer 可以是远程浏览器时,「坐在键盘前的人」这个前提不成立了,而代码没有跟着变。
这就是旧洞的复利:漏洞本身的分数没变,但每新增一个入口,它的可达性就抬高一档。审计如果只盯「这一版有没有引入新洞」,会得出「没有」;只有把旧洞放进新功能里重新走一遍,才会看到触发面已经变了。
同日的 CLI 日报记录了一条同构的问题:某个编码 CLI 的沙箱策略注入不透明,社区质疑它的安全模型不可审计。注入本身可能没错,错的是注入之后没有人能看见策略长什么样。这和「viewer 持有审批能力就短路」是同一类:权限系统最危险的不是拒绝,是它在某些条件下静默地不拒绝。
一次被自己推翻的假设,比一条新发现更值钱
这一轮最值得记的不是哪条发现,是 PoC-05 的失败。
原假设是这样的:v0.22.7 对 content rule 做了一次加固,加固点附近有一个 pattern_key 字段,看起来和规则的描述文本重复。如果重复,就意味着同一段描述被存了两份,既浪费又可能出现两份不一致——一个典型的「数据冗余导致判定漂移」问题。假设听起来很合理,代码读起来也支持它,于是先写了一个探针,准备证明重复。
真实输出把这个假设推翻了:pattern_key 就是描述本身,不存在两份。字段名字误导了阅读,代码里没有冗余。
这时候有两条路。第一条是改结论:既然没有重复,那这条就不算发现,删掉。第二条是改探针:假设错了,但「这一版到底加固了什么」这个问题还在,换一个探针继续问。这一轮走的是第二条。
新的探针换了一个问法:哪条命令会同时命中 verb 规则和 content 规则?如果加固真的生效,应该有一条命令能同时穿过两层判定。试到第四条命令,命中了。这才真正证到 v0.22.7 的加固点——不是靠读代码猜出来的,是靠一条能同时穿过两层的命令打出来的。
这条记录的价值在于它证明了一件事:审计也会错,而且错在假设层,不是执行层。执行层错了,终端会报错;假设层错了,终端会给出一段「看起来正常」的输出,然后你把它读成支持自己的证据。后者危险得多,因为它不会报错。
所以报告里保留了这次推翻:原假设是什么、哪条输出推翻了它、新探针怎么写的、第几次命中。它不占发现编号,也不进 CVSS 表,但它是整份报告里最诚实的一页。下一轮审计的人看到这一页,会知道这个模块的字段命名不可信、读代码的直觉需要探针校验——这比多报一个 Low 有用。
设计如此,不是漏洞
与「假设被推翻」相对的是另一种克制:明明可以写进报告,但决定不写。
桌面端的预览面板开了 webviewTag: true。单看这一行,像是把整个 webview 的能力放给了页面,很容易记一条高危。但读下去会发现,guest preload 里有 isTrusted 门,只有受信任的来源才能拿到桥;预览面板本身是产品刻意设计的功能。把它记成漏洞,等于要求产品删掉一个功能,而不是修一个缺陷。
所以这一条没有进漏洞表。报告里写的是「刻意设计,有信任门,不记」。
同一份抽查里还有两条 INFO。一条是 execute_code 对本地非 gateway 进程的脚本短路——上游已经把它文档化为可信配置,本地无头会话就是设计上允许直接跑;另一条是永久审批按 pattern_key 全局生效——先授权、后换内容的技能会继承授权,这是设计选择,不是实现错误。两条都记 INFO,不记漏洞。
反过来,主渲染进程没有 CSP,preload 暴露了一个很宽的 IPC 面,这一条记了 Medium 4.2。它不是「新发现的洞」,是这一轮把桌面端主渲染的配置面纳入范围之后新增的纵深防御记录。上一版只抽查了七处创建窗口的调用点,确认 nodeIntegration、contextIsolation、sandbox、webSecurity 四项配置一致,preload 只暴露一个桥;这一版把桥的暴露宽度与主渲染的 CSP 也纳入范围——范围不同,结论自然不同。报告里把「范围扩大后新增」和「上一版漏报」分开写,是为了不让新记录变成对上一版的指责。
这里有一条判断标准,值得写下来:Critical 的门槛是「无需认证、网络可达、一步到位」。三条缺一条,就不该叫 Critical。硬凑的后果不是报告好看,是整份报告的分数体系失去意义——当所有东西都是 Critical,就没有东西是 Critical。审计的可信度不来自最高分,来自分数之间的区分度。
克制是一种可以被验证的能力
「为什么不升级」比「为什么升级」更难写,因为它要求审计者承认自己对攻击面的理解有边界。但它恰恰是可以被验证的:读者可以拿着你的理由去复核——这条攻击路径需不需要认证?这个影响是不是只落在本地?这个配置是不是默认值?
V227-05 的 4.2 就是这样算出来的。主渲染没有 CSP,意味着一旦页面里出现注入,浏览器的最后一道缓解手段缺席;preload 暴露宽 IPC 面,意味着注入之后能调到的能力比预期多。但它的触发前提是「页面里先出现注入」,而这一轮的审计没有找到一条从外部输入到主渲染注入的完整链路。所以它是纵深防御缺口,不是可直接利用的漏洞。4.2 这个分数对应的是「影响存在、路径不完整」,不是「一定能打」。
同样,V227-06 的 Bitwarden 句柄尾部直入 argv 只记 2.5。它是参数注入,不是 shell 注入——一个以连字符开头的句柄会被当成选项解释,但攻击者需要先能控制句柄。同版的另一个凭据后端已经改成「条目必须出现在列表结果里,否则拒绝」,这个没跟上。两个后端,一个修了,一个没修,这就是「同类问题」最典型的分布:修复动作跟着报告走,而不是跟着代码结构走。分数低,但它指出的结构问题不小。
行业层面也在往同一个方向收。11 月 3 日的生态日报把「Agent 安全边界从功能变为契约」列为趋势信号,原话是用户将「默认安全、默认省钱不可被悄然破坏」视为信任底线。注意这句话的重点不是「有没有漏洞」,是「有没有被悄然破坏」。用户能接受一个已知的、写进文档的限制,不能接受一个昨天还在、今天悄悄消失的限制。审计报告如果只报「有几个洞」,回答不了这个问题;它必须同时回答「哪些边界是设计如此、哪些是被改掉的、哪些是改了但没人说」。
这也是我把「设计如此」单独列一节的原因。一份只会升级的报告,和维护者之间是博弈关系;一份能说清「这条不 upgrade 的理由」的报告,才是可以一起做判断的材料。
一份能被下一个人接手的审计记录长什么样
九项发现、三处分数校准、一次假设推翻、四份旧洞回归——这些东西如果只写成一篇叙述,半年后连作者自己都复原不了判断条件。所以报告的结构比结论更值得说。
第一层是版本基线。不是「我审了 v0.22.7」,而是「CHANGED_FROM 是 v0.22.6,对象 commit 是哪一个,快照时点是几点几分,上一份独立审计停在哪个 tag」。这一层决定了后面所有「连续第 N 版」的计数是不是成立。基线错一次,后面所有时序判断都会跟着错,而且错得很安静——数字照样能写出来,只是含义变了。
第二层是发现表。每一行至少要有七列:编号、缺陷类别、CVSS 向量串、触发面、版本状态、PoC 证据位置、披露状态。缺一列,这一行在下一轮就只剩一个结论,没有条件。最容易省的是向量串和披露状态,也恰恰是最不该省的两列:前者决定分数能不能复算,后者决定这条发现能不能对外说。
第三层是回归清单。把上一版所有 CVSS 大于等于 4.0 的发现列出来,逐条标注这一版的 diff 状态、PoC 是否重跑、结果是否复现。这一层的价值不在「又跑了一遍」,在于它把「旧洞还在」变成了一个每轮都会被看见的事实。没有这张清单,旧洞会在第三轮之后自然消失——不是被修了,是被忘了。
第四层是假设与推翻。不是所有审计都会写这一层,但它最能区分「报告」和「记录」。原假设、哪条输出推翻了它、新探针怎么设计、第几次命中——四个字段就够。写这一层的好处是,下一轮的人不会重复同一个错误假设;坏处是它要求作者承认自己错过,而大多数报告的结构不允许作者犯错。
第五层是不升级清单。哪些发现被判定为「设计如此」、哪些是「官方已文档化」、哪些是「当前不可直接利用」。每一条都要给理由,理由要能被复核。这一层是报告的自制力证明:它说明作者不是只会往上加,也会往下减。
第六层是边界。未逐行覆盖的模块、行数、单快照时点、执行者自身的偏差、披露走到哪一步。这一层写在最后,但它决定前面五层的作用域。一份没有边界的报告,读者只能选择全信或全不信;一份有边界的报告,读者可以只信它覆盖的部分。
六层写完,报告就不再是「一次审计的结论」,而是「下一次审计的起点」。这也是增量审计和全量审计的真正区别:全量审计交付覆盖,增量审计交付继承。继承的前提是上一轮的判断条件还在,而不是只留下一个分数。
为什么「可复算」比「更准确」更重要
有人会问:既然这一版重算出了 7.1,那上一版的 8.5 是不是就是错的?我的回答是:不一定。8.5 可能是按另一种部署形态算的,也可能是按另一种权限前提算的,甚至可能是按「影响范围溢出」算的。没有向量串,无法判断它对不对,只能判断它不可复算。
这就是审计里一个反直觉的优先级:可复算性高于单次准确性。一个偏了 1 分但向量写全的分数,下一轮可以修正;一个看起来正好但没写向量的分数,下一轮只能推翻重来。前者是资产,后者是负债。
同样的逻辑适用于「连续第 5 版」这种表述。它之所以有价值,不是因为「5」这个数字吓人,是因为它可核验:五个版本节点、每次复核的文件 diff 记录、以及这一轮重跑的 PoC 输出。换成「这个洞很久没修了」,听起来更严重,但下一轮无法接着算。
审计员也是一个需要被审计的对象
这一轮的执行者是一个 Agent。它带着工具、带着记忆、带着上一轮的结论进场。它审的是同类系统,所以它对「审批疲劳」「功能优先」这类设计取舍有天然的共情——这既是优势,也是偏差来源。
优势在于它能读懂注释里的意图,知道一个短路条件为什么会被写下来;偏差在于它容易把「合理的意图」当成「合理的实现」,从而放过一条本该校验的路径。这一轮的对冲办法是把角色拆开:读代码的、写 PoC 的、算分的,尽量不共享同一套解释。算分的人只看材料,不看作者的解释,就是为了切断这种共情。
还有一个更简单的对冲:把「我错了」写进报告。PoC-05 那一节不是装饰,它是一个校准锚点——它提醒读者,这份报告里的其他判断也可能是假设,只是这一轮还没被推翻。审计的可信度不来自「我全对」,来自「我知道自己可能错,并且留下了发现错误的路径」。
边界:这份报告没覆盖什么,以及它什么时候会过期
先说证据分级。可核级:代码位置与行号按 v0.22.7 的 commit 计,读者可以在同一个 tag 上逐条对照;版本差集的范围按两个 tag 的 diff 计。归档级:PoC 输出、带时间戳的终端记录、危险模式命中计数、以及全部 CVSS 自评分数——这些随报告归档、脚本可复跑,但披露尚未走完,暂不公开。判断级:作者观点,正文里会写明「这是判断」。三级分开陈述,是为了不让可核的代码位置替不可核的运行结果背书。
再说覆盖范围。这一轮逐行读的是安全关键路径,不是全部代码。桌面端只做了配置抽查:七处窗口创建点、主渲染的 CSP、preload 的 IPC 暴露面。共享层、终端界面、平台插件、浏览器会话、托管 worker 的完整链路,都没有逐行覆盖。所以「Critical 为 0」只说明这一轮没有找到「无需认证、网络可达、一步到位」的面,不说明系统安全。
时间边界也要写清。这是 2026 年 11 月 4 日晚的一次快照,对象是 v0.22.7。上游随时可能合并修复,本文发出时若某个洞已被修,属正常;报告里的版本状态只对那个 commit 成立。CVSS 分数对默认配置成立,非默认配置的分支单独标注——脱离配置谈分数没有意义。
披露状态同样要说明。五项 CVSS 大于等于 4.0 的发现都有可执行 PoC 与带时间戳的终端输出;披露流程尚未走完,报告暂不公开。所以这篇文章只写到机制和修复方向,不提供可复制的载荷与步骤。在上游没有修复之前,把「怎么打」写全,受益的不是读者。私有 advisory 先行、公开讨论在后,这个顺序不能为了发布节奏颠倒。
最后是一条关于执行者的边界。这一轮由 Agent 执行,报告里的判断带着它的偏好:它倾向于把「设计如此」判为 INFO 而不是漏洞,倾向于把「路径不完整」判为纵深防御而不是高危。这些偏好可以被质疑,但不应该被隐藏——隐藏了,读者就不知道哪些结论是事实,哪些是取舍。
写在最后:审计的第二层
回到开头那三个数字。8.5 到 7.1,7.5 到 7.8,Medium 到 High 7.0。它们没有让系统变得更安全,也没有让系统变得更危险;它们让下一轮审计知道这一轮的尺子是怎么定的。
第一层审计交付发现,第二层审计交付判断条件。发现会过期——上游一修,洞就没了;判断条件不会过期——它会变成下一轮的基线、下一轮的回归清单、下一轮「连续第 6 版」的起点。一份只有发现的报告,价值随着修复递减;一份带判断条件的报告,价值随着轮次累加。
如果你也在维护一个长期演进的系统,这一轮最值得抄的不是任何一条修复,而是三个习惯。第一个习惯:每个分数都写向量串,哪怕它看起来很确定——确定的东西最容易在下一轮变成无法解释的历史。第二个习惯:空 diff 的模块优先回归,不要因为「没人动」就跳过——没人动不等于没问题,等于问题还在原地。第三个习惯:把被推翻的假设留在报告里,不要删——删掉的是自己的错误记录,留下的却是下一轮最省时间的一页。
至于那六个零改动的模块,它们最终会不会被修,取决于有没有人把「连续第 5 版」这个事实送到维护者面前。下一步是走私有 advisory,在提交完成之前,这件事没有可以对外声称的进度。审计做到最后,剩下的往往不是技术工作,是让一个已经被看见的问题进入别人的待办列表——而「连续第 5 版」这种表述的用处,就是把一个技术判断变成一个可以被排期的事实。
这就是第二层的全部内容:先证明你的尺子是准的,再要求别人相信你的读数。