ARTICLE / 安全
官方技能工具的评审页把模型输出当代码执行:一个社区 PR 补上三个安全缺口
Anthropic 的 skill-creator 是官方技能仓库 anthropics/skills 里的元工具——用来创建和评测其他技能。它自带一个本地评审页:起一个小 HTTP 服务在 localhost,把评测输出渲染成网页让你打分,反馈存回本地文件。评测输出是谁写的?模型。模型输出是不可信输入,而评审页把它当代码处理了。2026 年 10 月 3 日,社区开发者 Joncik91 提交 PR #1961,标题直接点名四类问题:脚本逃逸、DNS rebinding、跨站 POST、转义不完整。这个 PR 到 10 月 11 日仍在推进,评论区有独立复核者用双版本对照实验逐条验证了修复效果。
对做 Agent 工具链的开发者,这篇有两个可迁移的东西:一是三个具体缺陷的成因模式(它们在任何"本地工具起网页渲染生成内容"的设计里都会复发),二是这次社区安全协作的验证方法——怎么证明一个安全修复真的修上了。只讨论已公开披露的内容与防御落点,面向授权安全研究。
三个缺口的成因,每一个都不需要恶意技能就能触发
缺口一:内嵌 JSON 的脚本逃逸。评审页把页面数据以 JSON 形式嵌进内联 <script> 标签。JSON 序列化不转义 < > 的话,字符串值里出现 </script> 就直接终止脚本块,后面的内容被浏览器当 HTML 解析——模型输出里只要"说"出这几个字符,注入就完成。攻击语义是经典的 XSS-in-embedded-JSON,但场景换了:过去这条链路的输入来自用户评论,现在来自你花大价钱调用的模型本身。修复是把 < > & 和 U+2028/U+2029 在嵌入 JSON 里一律转义成 \uXXXX,解析结果不变,注入面闭合。
缺口二:localhost 服务没有 Host/Origin 校验。评审页配套的本地 HTTP 服务器监听 feedback 接口。同浏览器里打开的任意网页——包括一个恶意页面——都能向这个端口 POST;更狠的是 DNS rebinding:攻击者页面先把域名解析到自己的服务器拿到 JS 执行环境,再把解析切到 127.0.0.1,同源策略就承认它对本地服务发起的请求。修复在 do_GET/do_POST 加了双条件:Host 必须是 localhost:<port> 或 127.0.0.1:<port>,Origin 若存在也必须落在这些主机上,否则 403;不带 Origin 的请求(curl、同源 GET)照常通过。后续追加的复审又补了一层:POST /api/feedback 要求 Content-Type: application/json,不匹配直接 415——这挡掉了浏览器跨站简单请求里能伪造 content type 的路径(form/文本类型),因为跨站无法发出带 JSON content type 的简单 POST,preflight 会被服务器拒掉。
缺口三:HTML 转义函数漏引号、字段漏转义。viewer.html 的 escapeHtml 没转义引号,metadata.timestamp、evals_run、runs_per_configuration、delta 字段、configLabel、run_number 等多个字段未经转义就进 innerHTML,包括一个 title 属性位。修复后 escapeHtml 覆盖 & < > " ' 五个字符、对 null/undefined 返回空串,未转义字段全部补上调用。这条的教训在于"有转义函数"和"所有插入点都用了转义函数"是两回事,属性上下文(title=)尤其容易被漏。
三个缺口互相独立,但共用一个根:本地工具的设计者默认 localhost 是个安全边界、默认模型输出"只是数据"。两个默认都不成立。localhost 挡不住浏览器里的任意页面;模型输出在渲染进 DOM 的那一刻就是代码候选。
这次值得抄的是验证方法,不只是修复本身
PR #1961 评论区做了一套可复用的"安全修复验收"流程,两条独立复核(另一次复核者给出的逐项矩阵)思路一致:
- 双版本对照:从 parent 提交和修复提交各生成一份页面,同环境跑。修复前版本上注入 payload(
</script><script>document.title='PWNED'</script>)验证逃逸确实成立,修复后版本同一 payload 只渲染为转义文本——先证明漏洞真实存在,再证明它被关掉了。 - 无头浏览器 DOM 断言:用 headless Chromium
--dump-dom检查生成页面,对计数区域塞合成<img>payload,统计原始注入元素从 2 个(parent)到 0 个(修复头)。 - 原始套接字请求矩阵:对本地服务器跑一组请求组合——
text/plainPOST(带/不带同源 Origin)应 415、缺 Content-Type 应 415、JSON POST(无 Origin 或同源)应 200、外部 Origin 或伪造 Host 应 403。关键结论是"fails closed":基线版本对这些矩阵用例返回 200,说明拦截精确等于声明的增量,不是碰巧。 - 补丁可应用性核验:
git apply --check在当前 head 通过,且增量 diff 应用后两个目标 blob 的哈希逐字节复现——修复没被 rebase 弄脏。 - 回归面确认:viewer 自己的两处 feedback fetch 都发送 JSON content type,自动保存与提交路径行为不变。
这套矩阵的价值在于:安全 PR 常见的失败模式不是"没修",而是"修了一半 + 顺手改坏了正常路径"。每一步都留下了可复核的证据,maintainer 只需要读结论。PR 作者也在描述里坦率标注了未覆盖面:同文件的另一份拷贝在 anthropics/claude-plugins-official 仓库里,尚未提 PR——同一个 skill-creator 代码存在于两个官方仓库,这本身就说明"生成物多处分发"的工具链需要跨仓库的修复同步机制,否则一个仓库修了、另一个还在漏。
检测与自查落点
如果你也做"本地起服务 + 网页渲染模型输出"的工具,四件事按优先级自查。嵌入 JSON 一律走安全序列化(转义尖括号、& 与行分隔符),不要用模板字符串拼接。本地服务不因为是 localhost 就免检:Host/Origin 白名单、写操作要求非简单请求头、默认拒绝。模型/日志/任何生成文本进 DOM 只有两条路——textContent 或经过属性上下文全覆盖的转义函数,innerHTML 拼接一律视为缺陷。验收按上面的矩阵做双版本对照,“修复后没报错"不等于"修复前有问题、修复后被挡住”。
供应链视角再补一句:skill-creator 这类元工具会被大量用户从官方仓库直接安装,它的安全缺陷会被乘上所有下游安装量。信任边界的问题在别处是单点,在元工具上是批发——同一个仓库的 issue #492 正好是硬币的另一面:一份对 580 多个已安装技能的自查审计发现,6 个社区技能被放置在 ~/.claude/skills/anthropic/ 目录下,目录名会让用户把它们当成官方技能,从而在审批高权限操作时放松警惕——其中若干技能申请的 allowed-tools 里赫然有 Bash 和 Edit。#492 末尾那句注记同样冷静:这些技能本身不含恶意代码,问题纯粹出在命名空间制造的信任边界上。官方目录的"官方"两个字是信任的载体,也是被冒用的对象;评审页的三个缺口则是载体自己漏了底。两类问题指向同一个治理方向:签名/验证的清单、保留命名空间、以及把"渲染生成内容"当不可信输入的工程默认。
本文面向授权安全研究与防御建设,未经授权对任何系统实施文中技术属违法行为。