JSON 工具箱

__proto__ 注入是怎么回事

结论:攻击者把 `__proto__` 当普通键传进来,深拷贝或合并函数一递归,原型就被改了。之后全站对象都多出攻击者控制的属性,轻则逻辑错乱,重则绕过权限判断。

深入约 5 分钟发布于 2026-10-30更新于 2026-10-30
原型链污染的原理与防护手段

结论:防护原型链污染,先过滤 __proto__、constructor、prototype 三个键,再用 Object.create(null) 承接用户输入,最后在 CI 加一条原型断言;只升级依赖库并不够。

一句话:输入里的 __proto__ 被当成普通键,合并函数一递归就改到了原型上。

先看一段能复现的代码

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

const input = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, input);

console.log({}.isAdmin);   // true

{}.isAdmin 本来是 undefined,现在变成了 true。攻击者没有改任何业务对象,只是污染了 Object.prototype,之后所有对象都带上了这个属性。

为什么 JSON.parse 不背锅

JSON.parse('{"__proto__": {"a": 1}}') 的结果里,__proto__ 是一个普通的自有属性,不是原型:

const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data);   // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype;  // true,原型没被改

安全的部分到此为止。危险在后面的「合并」「深拷贝」「按路径赋值」——这些操作会沿着键名去写,一旦键名是 __proto__,写的就是原型。

同样的坑还出现在 _.merge、Object.assign 的某些用法、jQuery.extend(true, ...)、以及大量手写的 set(obj, path, value) 工具函数里。

危害有多大

  • 权限绕过:if (user.isAdmin) 恒为真
  • 逻辑错乱:所有对象多出一个不该存在的字段,序列化后写进数据库
  • 拒绝服务:往原型上塞大对象,内存持续增长
  • 链式利用:配合模板引擎、命令执行库,可能升级为远程代码执行(CVE 里有大量此类案例)

四道防线

第一道:过滤危险键。 合并、赋值、路径设置之前统一拦掉三个键:

const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function isSafeKey(key) {
  return !FORBIDDEN_KEYS.has(key);
}

只过滤 __proto__ 不够——constructor.prototype 组合同样能改原型。

第二道:用无原型对象承接用户输入。

const safeTarget = Object.create(null);

没有原型链,__proto__ 就只是一个普通字符串键,写不进去任何地方。适合纯粹的数据容器。

第三道:用 Object.hasOwn 判断而不是 in。

if (Object.hasOwn(target, key)) { ... }   // 只看自有属性
if (key in target) { ... }                // 会被原型链影响

第四道:依赖库保持更新。 lodash 的 _.merge、deepmerge、jsonpatch 类库历史上都修过这个漏洞。用 npm audit 定期看,别把「库应该处理了」当默认前提。

检测方法

拿一份含 __proto__、constructor 的 JSON 过一遍你的合并逻辑,然后检查:

console.log({}.polluted);                      // 应该是 undefined
console.log(Object.prototype.polluted);        // 应该是 undefined

在 CI 里加一条这样的断言测试,成本极低,能挡住后续重构引入的回归。

常见问题

只过滤 __proto__ 这个键是否足够?

不够。constructor.prototype 组合同样能改到原型,必须把 __proto__、constructor、prototype 三个键一起拦掉。

Object.create(null) 能否彻底避免原型污染?

对纯数据容器有效:没有原型链,__proto__ 只是一个普通字符串键,写不到 Object.prototype。但它只适合承接数据,不能替代过滤与依赖升级。

怎么在 CI 里检测原型污染回归?

用一份含 __proto__、constructor 的 JSON 走一遍合并逻辑,再断言 {}.polluted 与 Object.prototype.polluted 都是 undefined。这条断言成本极低,能挡住后续重构引入的回归。

相关工具

延伸阅读