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

导语

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__ 这个名字,还要重新选择容器、限制键空间、区分自有属性与继承属性,并覆盖外部缓存和兼容实现。理解这些语言机制,才能在漏洞出现前识别同类代码。

相关推荐
Seraphina367 小时前
SQLI-labs(一)黑盒全流程
sql·安全·web安全
雷焰财经7 小时前
AI开始学会“越界”:当智能体拥有行动能力,人工智能的竞争已经进入安全深水区
人工智能·安全
花青泽14 小时前
Web安全信息收集--总
安全·web安全
jimmyleeee15 小时前
大模型安全之三十九:幻觉防线---- AI 幻觉检测、治理与预防指南
人工智能·安全
梦帮科技15 小时前
可证伪性工程测评与生产部署红蓝对抗:压力测试、长尾鲁棒性与全生命周期安全质检守卫
人工智能·深度学习·神经网络·安全·机器学习·自然语言处理·压力测试
姜鱼问生18 小时前
deploy.sh 部署脚本:健康检查 + 自动回滚
安全·github
福兮说18 小时前
前后端算的 MD5、SHA-256 对不上?编码、换行、BOM、HMAC、JSON 顺序,八个原因逐个实测
前端·javascript·node.js·json·哈希算法
其实防守也摸鱼18 小时前
渗透测试学习计划(全栈综合 · 零基础进阶)
android·数据库·学习·安全·oracle·自动化·学习方法
Black蜡笔小新18 小时前
EasyCVR从“人盯人”到“AI盯岗”:可视化大屏+智能告警,岗位管理一屏掌控
网络·安全·easycvr
kakakahahahaha18 小时前
电脑显示器无信号排查:HDMI/DP接口、独立显卡、内存与安全模式
windows·安全·电脑·笔记本电脑·内容运营·软件需求