__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;
}
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
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 的某些用法,以及大量手寫的 set(obj, path, value) 工具函式裡。
危害有多大
- 權限繞過:
if (user.isAdmin)恆為真 - 邏輯錯亂:所有物件多出不該存在的欄位,序列化後寫進資料庫
- 拒絕服務:往原型上塞大物件,記憶體持續增長
- 鏈式利用:配合範本引擎、命令執行函式庫,可能升級為遠端程式碼執行
四道防線
第一道:過濾危險鍵。
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
只過濾 __proto__ 不夠——constructor.prototype 組合同樣能改原型。
第二道:用無原型物件承接使用者輸入。
const safeTarget = Object.create(null);
沒有原型鏈,__proto__ 就只是一個普通字串鍵。
第三道:用 Object.hasOwn 判斷而不是 in。 in 會被原型鏈影響。
第四道:依賴函式庫保持更新。 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。這條斷言成本極低,能擋住後續重構引入的回歸。