utsuroi prototype pollution labs知識ベース 01 / 01
Prototype Pollution · Knowledge Base
01

Prototype Pollution の基礎

When pollution flows up the prototype chain

JavaScript はプロトタイプベースの言語。全オブジェクトは Object.prototype を継承しており、これを攻撃者が書き換えられると、プログラム全体に影響するというユニークな攻撃面が生まれます。

1. JavaScript のプロトタイプ #prototype

JavaScript で obj.someProperty にアクセスすると、obj 自身に無ければ obj.__proto__(= Object.prototype)を見に行く。これは継承のメカニズム。

const user = { name: 'miharu' };
console.log(user.name);       // 'miharu' (自身のプロパティ)
console.log(user.toString);   // function (Object.prototype から継承)

// __proto__ で上位プロトタイプへの参照
console.log(user.__proto__ === Object.prototype);  // true

2. 汚染の仕組み #pollution

攻撃者が obj.__proto__.someProp = "evil" という操作をできれば、すべての Object に影響

// 汚染前
const a = {};
const b = {};
console.log(a.isAdmin);  // undefined
console.log(b.isAdmin);  // undefined

// 攻撃者が __proto__ を書き換え
({}).__proto__.isAdmin = true;

// 汚染後
console.log(a.isAdmin);  // true ← !!!
console.log(b.isAdmin);  // true ← !!!
console.log(({}).isAdmin); // true

これが攻撃に発展するのは、アプリ側で「あるオブジェクトに isAdmin プロパティがあるか」のような判定をしている場合。判定ロジックが汚染で誤動作する。

3. 汚染源(危険なパターン) #sources

攻撃者が __proto__constructor.prototype をキーとして含む JSON を送ると、サーバ側のコードが「再帰的マージ」「動的プロパティ代入」を行う際に汚染が発生する。

// 危険なマージ実装
function merge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object') {
            target[key] = target[key] || {};
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

// 攻撃者の入力
const evil = JSON.parse('{"__proto__":{"isAdmin":true}}');
merge({}, evil);
// → Object.prototype.isAdmin = true がセットされる

有名な汚染源:lodash の _.merge / _.set(古いバージョン)、jQuery の $.extend、MongoDB の $set など。

4. 影響範囲 #impact

5. 正しい防御 #defense

  1. __proto__ / constructor / prototype をキーとして拒否 — JSON を受け取った時点で、これらをキーに含むなら 400
  2. プロトタイプ汚染対策済みのライブラリを使う — lodash 4.17.12+、jQuery 3.4.0+ 等の最新版
  3. Object.freeze(Object.prototype) — アプリ起動時に Object.prototype を凍結。後続の汚染試行が無効化(他ライブラリの動作に影響する場合があるので慎重に)
  4. Map を使う — 動的キーを扱う場面では {} ではなく new Map()。プロトタイプチェーンと無関係
  5. Object.create(null) を使う — プロトタイプを持たないオブジェクト。pollution の影響を受けない
  6. Schema 検証 — Joi、Zod、Ajv 等で受け入れるオブジェクト構造を厳格化
// JSON.parse の reviver で危険キーを除去
const safe = JSON.parse(input, (key, value) => {
    if (key === '__proto__' || key === 'constructor') return undefined;
    return value;
});