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
- 認可バイパス —
if (user.isAdmin)が常に true に - テンプレートインジェクション → RCE — Pug、EJS 等のテンプレートエンジンで内部プロパティが書き換わると、コード実行に至るケースがある
- DoS — JavaScript エンジン全体の挙動が変わるので、想定外の例外で全体が落ちる
- XSS — クライアントサイドの prototype pollution(URL ハッシュ等経由)で DOM ベース XSS
5. 正しい防御 #defense
- __proto__ / constructor / prototype をキーとして拒否 — JSON を受け取った時点で、これらをキーに含むなら 400
- プロトタイプ汚染対策済みのライブラリを使う — lodash 4.17.12+、jQuery 3.4.0+ 等の最新版
- Object.freeze(Object.prototype) — アプリ起動時に Object.prototype を凍結。後続の汚染試行が無効化(他ライブラリの動作に影響する場合があるので慎重に)
- Map を使う — 動的キーを扱う場面では
{}ではなくnew Map()。プロトタイプチェーンと無関係 - Object.create(null) を使う — プロトタイプを持たないオブジェクト。pollution の影響を受けない
- Schema 検証 — Joi、Zod、Ajv 等で受け入れるオブジェクト構造を厳格化
// JSON.parse の reviver で危険キーを除去
const safe = JSON.parse(input, (key, value) => {
if (key === '__proto__' || key === 'constructor') return undefined;
return value;
});