修复原型污染,为什么既要换容器又要拒绝危险键?

导语

面对原型污染,最直觉的补丁是:遇到 __proto__ 就拒绝。

但 yayson CVE-2026-61534 的官方修复没有停在一个 if。补丁引入无原型对象,拒绝三类危险键,把调用方缓存规范化为安全容器,将 for...in 改为 Object.keys(),并同时修改现代 Store 和 LegacyStore。

为什么需要这么多层?因为漏洞不只存在于一个输入值,而是分布在容器创建、外部对象注入、关系遍历和兼容代码之间。本文从补丁工程角度拆解每道防线解决的具体问题。


一、漏洞与版本事实

  • CVE-2026-61534 影响 yayson <=4.2.0,修复于 4.3.0

  • 受影响组件包括 StoreLegacyStore

  • 文档控制的 typeid 与关系名被用作普通对象键,可能污染 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 测试,没有变体矩阵。

更完整的验收问题

  1. 所有动态键汇点是否使用安全容器?

  2. 危险键是否在进入模型前统一拒绝?

  3. 外部对象是否验证或规范化?

  4. 遍历是否只读取自有属性?

  5. 主路径、关联路径和 legacy 路径是否一致?

  6. 测试是否断言全局原型没有变化?


九、团队行动清单

P0

  • 升级 yayson 到 4.3.0+

  • 无法升级时,先做统一危险键拦截,并评估 --disable-proto=throw

  • 重启可能已污染的长驻进程。

P1

  • 在代码库搜索 {} 字典、动态键赋值、for...in 和深度合并。

  • 检查调用者可传缓存、适配器和类型映射的接口。

  • 为 modern/legacy、data/included 建立交叉测试矩阵。

P2

  • 把“安全容器 + 键约束 + 自有属性遍历”做成共享工具函数。

  • 在安全补丁评审中要求变体搜索记录。

  • 用属性测试随机生成危险键和嵌套关系。

  • 对原型变化设置测试进程级断言和清理机制。


总结

yayson 4.3.0 的价值在于,它没有把原型污染当成单个字符串过滤问题,而是建立了组合安全不变量:容器不继承原型、危险键不进入模型、外部缓存必须规范化、遍历只处理自有属性、新旧实现保持一致。

对安全修复团队而言,这是一种可复用的方法:从触发样本出发,寻找所有共享根因与旁路,再把补丁从“阻止这一条输入”提升为“让一类状态无法成立”。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值