导语
JavaScript 对象太方便了:缓存、索引、注册表、配置和分组结果都能用 {} 快速实现。问题是,普通对象从来不只是一张空表;它背后还有原型链。
yayson CVE-2026-61534 把这个语言细节变成了现实漏洞。JSON 的资源类型和编号被用于普通对象的动态键,特殊名称可让内部查找落到 Object.prototype。污染一旦发生,同一进程中的其他普通对象也可能读到攻击者设置的继承属性。
本文从 JavaScript 语言机制出发,比较普通对象、无原型对象与 Map 的安全差异。
一、已确认事实
-
yayson
<=4.2.0的Store和LegacyStore受 CVE-2026-61534 影响。 -
最低修复版本为
4.3.0,2026 年 6 月 22 日发布。 -
官方根因是把文档控制的
type、id和关系名用于普通对象查找表。 -
修复拒绝
__proto__、constructor、prototype,采用Object.create(null),使用Object.keys(),并规范化外部缓存。 -
项目公告显示 High/8.1,GitHub 数据库显示 Critical/9.1。
-
确定影响是逻辑污染与拒绝服务;更高影响依赖应用 gadget。
-
截至核验时,一手来源未确认在野利用。
二、三个容易混淆的概念
自有属性
对象自身直接保存的属性:
const o = { role: 'reader' }
Object.hasOwn(o, 'role') // true
继承属性
对象沿原型链获得的属性:
Object.prototype.shared = true
const o = {}
o.shared // true
Object.hasOwn(o, 'shared') // false
delete Object.prototype.shared
动态键
运行时由变量决定的属性名:
o[userControlledKey] = value
动态键不是漏洞;当键不可信、容器带原型、代码又没有限制键空间时,风险才出现。
三、yayson 中的关键语义错位
受影响逻辑可抽象为:
if (!models[type]) models[type] = {}
models[type][id] = model
开发者的心智模型是:
models 不存在某个 type → 创建子字典 → 保存 id
JavaScript 的实际行为还包含:
models 自身没有该键 → 沿原型链查找 → 可能得到 Object.prototype
所以"是否存在"与"是否为自有业务分组"并不是同一个问题。
四、三类字典实现如何选择
普通对象 {}
优点是语法简单、序列化方便;缺点是继承原型,接收不可信键时必须额外防护。
适用:固定、由开发者定义的字段集合。
无原型对象 Object.create(null)
没有 Object.prototype,字符串键不会自动解析到继承成员。缺点是缺少 toString 等常规方法,一些库可能假设对象具有普通原型。
适用:需要对象接口、键来自外部的纯查找表。
Map
键和值语义明确,不通过对象原型解析;API 也能区分 has() 与 get()。缺点是不能直接按普通 JSON 对象序列化,迁移成本可能更高。
适用:内部缓存、注册表和不要求对象序列化的索引。
五、无害对比实验
'use strict'
function plainDictionary(key) {
const dict = {}
return {
inheritedValue: dict[key],
isOwn: Object.hasOwn(dict, key),
}
}
function nullDictionary(key) {
const dict = Object.create(null)
return {
value: dict[key],
isOwn: Object.hasOwn(dict, key),
}
}
console.log(plainDictionary('__proto__'))
// inheritedValue 是对象,isOwn 为 false
console.log(nullDictionary('__proto__'))
// value 为 undefined,isOwn 为 false
该实验不修改任何全局原型,只展示相同键在不同容器中的读取差异。
六、为什么只换容器还不够
无原型对象可以阻断 __proto__ 通过继承链解析,但应用仍可能把危险键复制到普通模型、配置对象或其他组件。constructor 与 prototype 也可能参与更复杂的属性路径。
因此官方补丁同时:
-
使用无原型容器;
-
拒绝三类危险成员名;
-
将外部普通缓存复制进安全容器;
-
用
Object.keys()避免枚举继承属性。
这是一套"减少危险语义 + 缩小键空间 + 阻断传播"的组合策略。
七、代码审计检查表
写入侧
-
外部值是否出现在方括号属性写入中?
-
深度赋值工具是否允许
a.b.c形式路径? -
对象合并是否递归处理
constructor.prototype? -
缓存和注册表是否使用普通对象?
读取侧
-
权限、功能开关和危险参数是否直接读取
obj.flag? -
是否需要
Object.hasOwn()? -
是否使用
for...in枚举继承属性? -
默认值逻辑是否把继承属性当作用户设置?
边界侧
-
调用方能否注入自定义缓存?
-
序列化/反序列化会不会把安全容器重新变成普通对象?
-
modern 与 legacy 实现是否应用相同规则?
八、团队行动建议
P0
-
升级 yayson 至
4.3.0+。 -
无法升级时阻断危险键,并评估
--disable-proto=throw。 -
重启处理过不可信数据的长驻 Node.js 进程。
P1
-
将不可信键字典迁移到
Map或无原型对象。 -
对安全判断采用自有属性检查。
-
搜索
for...in与动态键写入的组合。 -
检查
data、included、关系和外部缓存入口。
P2
-
为公共数据处理库建立危险键测试集。
-
在测试前后比较
Object.prototype自有属性。 -
对合并、路径赋值、缓存注入进行模糊测试。
-
把容器类型作为安全设计评审项记录下来。
总结
{} 看起来是空对象,却不是没有语义的空字典。只要外部字符串进入动态属性操作,原型链就必须进入威胁模型。
yayson 的修复说明,稳健防御不能只记住 __proto__ 这个名字,还要重新选择容器、限制键空间、区分自有属性与继承属性,并覆盖外部缓存和兼容实现。理解这些语言机制,才能在漏洞出现前识别同类代码。