本文 AI 產出,尚未審核

JavaScript 安全與防護 – 原型污染(Prototype Pollution)


簡介

Node.js 與前端 JavaScript 應用程式中,物件的原型(prototype)是語言最核心的機制之一。它讓我們可以在所有同類型的物件上共享方法與屬性,提升開發效率與程式碼可重用性。
然而,正因為原型是 全域共享 的結構,若被惡意或不小心 修改,就會產生 原型污染(Prototype Pollution) 的安全漏洞。

原型污染不僅會導致 資料完整性 被破壞,還可能被利用執行 任意程式碼、繞過權限驗證,甚至造成 Denial‑of‑Service(DoS)。因此,了解其原理、辨識攻擊向量、以及落實防護措施,是每位 JavaScript 開發者在 安全與防護 這一單元必須掌握的能力。


核心概念

1. 什麼是原型污染?

JavaScript 中每個物件都有一條隱藏的鏈([[Prototype]]),指向其 原型物件。當我們讀取或寫入一個不存在於本身的屬性時,JavaScript 會沿著原型鏈向上查找。如果透過 外部輸入(例如 JSON.parsereq.body)直接寫入 __proto__prototypeconstructor 等特殊屬性,就會 改變整個原型鏈,進而影響所有同類型的物件。

簡單比喻:想像所有汽車都有一個共用的「說明書」物件作為原型。若有人在說明書上寫下「所有汽車的速度上限改為 0」,那麼所有汽車都會受到影響。這就是原型污染的概念。


2. 攻擊向量

來源 常見情境 可能的污染屬性
JSON.parse 直接解析外部 JSON {"__proto__":{"admin":true}}
Object.assign / _.merge(lodash) 合併使用者提供的物件 {"constructor":{"prototype":{"isAdmin":true}}}
表單/URL 參數 Express、Koa 等框架自動解析 ?__proto__[isAdmin]=true
npm 套件 config, mongoose 等會深層合併設定 內部使用 mergeextend 等函式

重點:只要程式碼在 深度合併(deep merge)或 遞迴遍歷 物件時,沒有對 __proto__prototypeconstructor 進行過濾,就有可能被污染。


3. 為什麼會影響整個系統?

一旦原型被污染,所有 之後新建立的物件(只要與該原型相同)都會自動擁有被污染的屬性。例如:

// 假設全域 Object.prototype 已被污染
console.log({}.isAdmin); // true

這會導致:

  • 授權繞過:檢查 if (user.isAdmin) 變成永遠成立。
  • 資料錯誤:程式依賴 hasOwnProperty 判斷屬性是否自有,卻被偽造。
  • DoS:在原型上加入巨大的陣列或函式,導致記憶體耗盡。

4. 程式碼範例

以下提供 5 個實用範例,說明原型污染的產生、危害與防禦方式。

範例 1️⃣:最簡單的污染示範

// 直接透過物件字面量寫入 __proto__
const payload = JSON.parse('{"__proto__":{"isAdmin":true}}');

// 觸發污染
Object.assign({}, payload);

// 之後任何新物件都會繼承 isAdmin
console.log({}.isAdmin); // => true

說明Object.assign 會把 payload 的所有可列舉屬性(包括 __proto__)複製到目標物件,導致原型被改寫。


範例 2️⃣:利用深層合併(lodash merge)的漏洞

const _ = require('lodash');

// 攻擊者送出的深層物件
const malicious = {
  constructor: {
    prototype: {
      isSuperUser: true
    }
  }
};

// lodash 深度合併
const config = _.merge({}, malicious);

console.log({}.isSuperUser); // => true

說明lodash.merge 會遞迴遍歷所有屬性,若不過濾 constructor.prototype,同樣會污染原型。


範例 3️⃣:Express 中的表單參數污染

// 假設使用 body-parser 解析 JSON
app.post('/login', (req, res) => {
  // req.body 可能被污染
  const user = Object.assign({}, req.body);

  // 授權檢查
  if (user.isAdmin) {
    // 原本只有管理員才會進到這裡
    res.send('Welcome, admin!');
  } else {
    res.send('Access denied');
  }
});

若客戶端送出:

POST /login
Content-Type: application/json

{
  "__proto__": { "isAdmin": true }
}

所有 user 物件都會有 isAdmin: true,攻擊者即可繞過授權。


範例 4️⃣:防禦 – 透過 Object.create(null) 建立「乾淨」物件

function safeAssign(target, source) {
  // 建立沒有原型的乾淨物件
  const clean = Object.create(null);
  for (const key of Object.keys(source)) {
    // 忽略危險屬性
    if (['__proto__', 'prototype', 'constructor'].includes(key)) continue;
    clean[key] = source[key];
  }
  return Object.assign(target, clean);
}

// 使用方式
const safeUser = safeAssign({}, req.body);

說明Object.create(null) 產生的物件 沒有原型,即使不小心寫入 __proto__ 也不會影響全域 Object.prototype


範例 5️⃣:使用 proxy 監控並阻擋危險屬性

function createSafeObject(obj) {
  return new Proxy(obj, {
    set(target, prop, value) {
      if (['__proto__', 'prototype', 'constructor'].includes(prop)) {
        throw new Error(`Attempt to modify protected property: ${prop}`);
      }
      return Reflect.set(target, prop, value);
    }
  });
}

// 範例
const safeConfig = createSafeObject({});
safeConfig.foo = 'bar';          // ✅ 正常
safeConfig.__proto__ = {};       // ❌ 會拋出錯誤

說明Proxy 可以在屬性寫入時即時檢查,對於大型專案或第三方套件不易改寫的情況特別有用。


常見陷阱與最佳實踐

陷阱 可能的後果 建議的防禦措施
直接使用 Object.assign_.mergedeepmerge 原型被不經意污染 過濾 __proto__prototypeconstructor;或使用 Object.create(null)
依賴 hasOwnProperty 判斷屬性 被偽造的屬性繞過檢查 使用 Object.prototype.hasOwnProperty.call(obj, key),或在 安全的物件 上檢查
將使用者輸入直接序列化成物件 JSON 解析時自動污染 禁用 JSON.parse 的 reviver,或在解析後 深度遍歷 檢查危險鍵
使用舊版第三方套件(如 lodash < 4.17.21) 已知 merge 漏洞 升級 至最新版本;或自行 實作安全合併
在全域範圍(global、window)掛載自訂屬性 可能被污染的原型影響全局 儘量 封裝 在模組或 IIFE,避免直接修改全域原型

具體最佳實踐

  1. 輸入驗證:對所有外部輸入(HTTP、檔案、CLI)使用 白名單 檢查,絕不允許 __proto__prototypeconstructor 等關鍵字。
  2. 安全合併:使用 Object.assign 前先 剔除危險屬性,或改用 deepmergeisMergeableObject 回呼過濾。
  3. 最小權限原則:不要在 全域Object.prototype)上掛載自訂屬性,改為 專屬類別Symbol
  4. 升級依賴:定期檢查 npm 套件安全公告,特別是常用的 深度合併設定管理 套件。
  5. 自動化測試:加入 原型污染測試案例(例如使用 npm auditsnyk)於 CI/CD 流程。

實際應用場景

場景 為何會出現原型污染 防護要點
設定檔合併configdotenv 多層設定檔使用 merge 合併使用者自訂檔 只允許 白名單 鍵;在合併前使用 Object.create(null)
ORM / ODM(如 mongoose 讀取資料庫文件時自動映射為 POJO,若文件包含 __proto__ 會污染模型 在模型層過濾危險屬性;使用 lean() 取得純資料後自行清理
API Gateway(Node.js + Express) 代理多個微服務,將請求體直接轉發或合併 深度檢查 請求體;使用 JSON Schema 驗證
前端狀態管理(Redux、MobX) 從伺服器載入初始狀態,直接 Object.assign(state, payload) 在 reducer 中 clone過濾,或使用 immutable.js
第三方套件(如 lodash.merge 套件內部未處理危險屬性 自行 fork包裝 該函式,加入過濾機制

總結

原型污染是 JavaScript 生態系統 中一個隱蔽但極具危害性的漏洞。只要程式碼在 深度合併解析外部物件 時未對 __proto__prototypeconstructor 進行嚴格過濾,就可能讓整個應用程式的安全基礎被顛覆。

透過本文的 概念說明實作範例、以及 最佳實踐,讀者應該能:

  • 清楚辨識出可能的 原型污染入口
  • 掌握 防禦技巧(過濾危險鍵、使用 Object.create(null)、Proxy 監控等);
  • 實務專案 中落實 安全合併與驗證,降低被利用的風險。

最後提醒:安全不是一次性的設定,而是 持續的檢視與改進。在每一次引入新套件、改寫資料流或擴充 API 時,都請重新檢查是否有可能產生原型污染,並將相應的防護機制寫入程式碼與測試中。只有這樣,我們才能在 JavaScript 的自由與強大之間,維持應用程式的 可信任穩定