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;
}

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。這條斷言成本極低,能擋住後續重構引入的回歸。

延伸閱讀