__proto__ 注入とは何か
要点:JSON.parse は __proto__ を普通のキーとして扱います。損傷は Object.prototype に書き込む再帰マージで起き、以後すべてのオブジェクトが攻撃者のプロパティを継承します。

要点:マージの前に __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
業務オブジェクトは触られていません。Object.prototype が触られ、すべてのオブジェクトが isAdmin を運ぶようになりました。
JSON.parse が悪者でない理由
const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data); // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype; // true、未変更
__proto__ は普通の own プロパティとして届きます。危険は完全に、後でマージ、深いコピー、パスによる設定をするコードにあります。_.merge、一部の Object.assign パターン、無数の手書き set(obj, path, value) ヘルパーです。
影響
- 認可バイパス:
if (user.isAdmin)が無条件に真になる - データ破損:予期しないフィールドが保存レコードにシリアライズされる
- サービス拒否:プロトタイプに付けられた大きなオブジェクトがメモリを恒久的に増やす
- 昇格:テンプレートエンジンやコマンドランナーと組み合わさり、リモートコード実行に至った例がある
四つの防御
キーをフィルタする。
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
__proto__ だけを遮断するのでは不十分です。constructor.prototype が同じ場所に到達します。
null プロトタイプのオブジェクトへマージする。
const safeTarget = Object.create(null);
プロトタイプチェーンがなければ、__proto__ はただの文字列キーです。
in ではなく Object.hasOwn を使う。 in はプロトタイプチェーンを参照します。
依存をパッチ済みに保つ。 lodash、deepmerge、json-patch はこの種のバグの修正を出してきました。npm audit を監視してください。
修正が保たれることを検証する
__proto__ と constructor を含むペイロードをマージに通し、表明します。
console.log({}.polluted); // undefined
console.log(Object.prototype.polluted); // undefined
CI の表明一つ、ほぼゼロコストで、後のリファクタリングによる回帰を捕まえます。
FAQ
マージはどのキーをフィルタすべきですか?
__proto__、constructor、prototype をまとめて遮断します。__proto__ だけのフィルタでは不十分で、constructor.prototype が同じプロトタイプに到達します。
Object.create(null) が書き込みを止めるのはなぜですか?
null プロトタイプのオブジェクトにはプロトタイプチェーンがないので、__proto__ は普通の文字列キーのままです。データコンテナとしては機能しますが、キーフィルタや依存の更新を置き換えるものではありません。
CI でプロトタイプ汚染をテストするには?
__proto__ と constructor を含むペイロードをマージに通し、{}.polluted と Object.prototype.polluted が undefined であることを表明します。表明一つはほとんどコストがかからず、後のリファクタリングによる回帰を捕まえます。