导语
面对原型污染,最直觉的补丁是:遇到 __proto__ 就拒绝。
但 yayson CVE-2026-61534 的官方修复没有停在一个 if。补丁引入无原型对象,拒绝三类危险键,把调用方缓存规范化为安全容器,将 for...in 改为 Object.keys(),并同时修改现代 Store 和 LegacyStore。
为什么需要这么多层?因为漏洞不只存在于一个输入值,而是分布在容器创建、外部对象注入、关系遍历和兼容代码之间。本文从补丁工程角度拆解每道防线解决的具体问题。
一、漏洞与版本事实
-
CVE-2026-61534 影响 yayson
<=4.2.0,修复于4.3.0。 -
受影响组件包括
Store和LegacyStore。 -
文档控制的
type、id与关系名被用作普通对象键,可能污染Object.prototype。 -
官方合并提交涉及 5 个文件,新增和修改 200 余行,包括实现与回归测试。
-
项目公告为 High/建议 8.1,GitHub 数据库为 Critical/9.1。
-
确定影响与条件性升级必须区分;截至核验时未确认在野利用。
二、第一层:换掉不安全的字典容器
补丁增加:
export function safeObject() {
return Object.create(null)
}
无原型对象不会从 Object.prototype 继承 __proto__ 等成员,更符合“字符串到值”的纯字典语义。
它解决的是容器根因:即使开发者漏掉某个键校验,查找也不会自动落入共享原型。
但它并不能保证这些键以后不会被复制到普通业务模型,所以还需要第二层。
三、第二层:明确拒绝危险成员名
补丁定义危险键集合:
__proto__
constructor
prototype
拒绝策略解决的是数据契约问题:这些名称不应作为文档驱动的模型或关系成员继续流转。
只屏蔽 __proto__ 往往不够,因为某些深度属性赋值或合并逻辑可能沿 constructor.prototype 到达原型。明确收缩键空间也能防止安全对象被后续代码转换后重新产生风险。
四、第三层:规范化调用者传入的缓存
公共 API 的难点是,容器不一定由库自己创建。调用方可能传入:
const models = {}
store.find('articles', '42', models)
如果内部默认安全,但外部缓存仍是普通对象,漏洞可能通过扩展点重新进入。
补丁中的 safeCache() 会:
-
缓存缺失时创建无原型对象;
-
已是无原型对象时保留其身份;
-
普通对象则复制到新的无原型对象。
这道防线体现了一个重要原则:安全不变量必须覆盖依赖注入和扩展接口,而不只是内部默认值。
五、第四层:停止枚举继承属性
for...in 会遍历对象自身和继承的可枚举属性。进程若已经发生污染,循环可能把继承成员再次当作关系或资源传播。
补丁将相关循环改为:
for (const key of Object.keys(input)) {
// 只处理自有可枚举属性
}
这是“污染后传播控制”。即使其他组件已污染原型,yayson 也尽量不把继承属性吸收进自己的数据模型。
六、第五层:现代与兼容实现同步修复
项目同时保留 Store 与 LegacyStore。安全修复如果只覆盖现代路径,旧格式仍可能成为旁路。
官方提交把安全辅助函数应用于两个实现,并为二者增加测试。这提示团队:发现漏洞后,变体搜索至少应覆盖:
-
新旧协议实现;
-
同步与异步入口;
-
单对象与集合入口;
-
默认容器与外部容器;
-
主资源与关联资源。
七、无害修复对比模型
const BLOCKED = new Set(['__proto__', 'constructor', 'prototype'])
function vulnerablePut(dict, key, value) {
dict[key] = value
}
function hardenedPut(dict, key, value) {
if (BLOCKED.has(key)) throw new Error('unsafe key')
if (Object.getPrototypeOf(dict) !== null) {
throw new Error('unsafe dictionary container')
}
dict[key] = value
}
const safe = Object.create(null)
hardenedPut(safe, 'articles', Object.create(null))
console.log(Object.keys(safe)) // ['articles']
这个模型不污染原型,只展示修复后的两个前置条件:键必须安全,容器也必须安全。
八、如何评审一个原型污染补丁
不充分的修复信号
-
只在 HTTP 路由过滤一个字段;
-
只屏蔽
__proto__; -
仅修改默认缓存,忽略调用方注入;
-
保留
for...in处理不可信对象; -
只有单一 PoC 测试,没有变体矩阵。
更完整的验收问题
-
所有动态键汇点是否使用安全容器?
-
危险键是否在进入模型前统一拒绝?
-
外部对象是否验证或规范化?
-
遍历是否只读取自有属性?
-
主路径、关联路径和 legacy 路径是否一致?
-
测试是否断言全局原型没有变化?
九、团队行动清单
P0
-
升级 yayson 到
4.3.0+。 -
无法升级时,先做统一危险键拦截,并评估
--disable-proto=throw。 -
重启可能已污染的长驻进程。
P1
-
在代码库搜索
{}字典、动态键赋值、for...in和深度合并。 -
检查调用者可传缓存、适配器和类型映射的接口。
-
为 modern/legacy、data/included 建立交叉测试矩阵。
P2
-
把“安全容器 + 键约束 + 自有属性遍历”做成共享工具函数。
-
在安全补丁评审中要求变体搜索记录。
-
用属性测试随机生成危险键和嵌套关系。
-
对原型变化设置测试进程级断言和清理机制。
总结
yayson 4.3.0 的价值在于,它没有把原型污染当成单个字符串过滤问题,而是建立了组合安全不变量:容器不继承原型、危险键不进入模型、外部缓存必须规范化、遍历只处理自有属性、新旧实现保持一致。
对安全修复团队而言,这是一种可复用的方法:从触发样本出发,寻找所有共享根因与旁路,再把补丁从“阻止这一条输入”提升为“让一类状态无法成立”。

322

被折叠的 条评论
为什么被折叠?



