_proto__ 不是普通键:从 JSON 反序列化看原型污染

导语

JavaScript 对象太方便了:缓存、索引、注册表、配置和分组结果都能用 {} 快速实现。问题是,普通对象从来不只是一张空表;它背后还有原型链。

yayson CVE-2026-61534 把这个语言细节变成了现实漏洞。JSON 的资源类型和编号被用于普通对象的动态键,特殊名称可让内部查找落到 Object.prototype。污染一旦发生,同一进程中的其他普通对象也可能读到攻击者设置的继承属性。

本文从 JavaScript 语言机制出发,比较普通对象、无原型对象与 Map 的安全差异。


一、已确认事实

  • yayson <=4.2.0StoreLegacyStore 受 CVE-2026-61534 影响。

  • 最低修复版本为 4.3.0,2026 年 6 月 22 日发布。

  • 官方根因是把文档控制的 typeid 和关系名用于普通对象查找表。

  • 修复拒绝 __proto__constructorprototype,采用 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__ 通过继承链解析,但应用仍可能把危险键复制到普通模型、配置对象或其他组件。constructorprototype 也可能参与更复杂的属性路径。

因此官方补丁同时:

  • 使用无原型容器;

  • 拒绝三类危险成员名;

  • 将外部普通缓存复制进安全容器;

  • 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 与动态键写入的组合。

  • 检查 dataincluded、关系和外部缓存入口。

P2

  • 为公共数据处理库建立危险键测试集。

  • 在测试前后比较 Object.prototype 自有属性。

  • 对合并、路径赋值、缓存注入进行模糊测试。

  • 把容器类型作为安全设计评审项记录下来。


总结

{} 看起来是空对象,却不是没有语义的空字典。只要外部字符串进入动态属性操作,原型链就必须进入威胁模型。

yayson 的修复说明,稳健防御不能只记住 __proto__ 这个名字,还要重新选择容器、限制键空间、区分自有属性与继承属性,并覆盖外部缓存和兼容实现。理解这些语言机制,才能在漏洞出现前识别同类代码。

相关推荐
Wang's Blog2 小时前
Java框架快速入门: Spring Security+OAuth2之方法级安全注解
java·安全·spring
mounter6254 小时前
统一安全域:KVM Planes 如何驾驭硬件特权级
linux·安全·linux kernel·kernel
代码中介商4 小时前
JSON 完全指南:从语法到项目实战
json
sbjdhjd4 小时前
ThinkPHP 5.0.10 缓存写入型 RCE 复盘:从手动搭建、换行绕过到源码单步验证 | 05
java·后端·安全·spring·网络安全·数据挖掘·php
名字还没想好☜5 小时前
Go 用 json.Decoder 流式解析大 JSON:边读边处理不爆内存
开发语言·后端·golang·go·json
QXWZ_IA6 小时前
电力作业安全管控如何从人防走向技防智防?智能工器具体系解析
人工智能·科技·安全
gf13211116 小时前
【python_发送飞书卡片消息_直接组装JSON格式发送卡片】
python·json·飞书
小程序设计7 小时前
校园网络资产自动探测与安全状态评估系统设计与实现
网络·安全
小程序设计7 小时前
电力大模型输出内容安全风险评估和控制系统
安全