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

结论:防护原型链污染,先过滤 __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。这条断言成本极低,能挡住后续重构引入的回归。
相关工具
- JSON 格式化与校验:本地解析,可以直观看到
__proto__作为普通键出现在哪一层 - JSON 处理中的安全风险清单:XSS、深度炸弹、日志泄漏等同类问题