flawopen.com/Prototype Pollution/Object.assign()
Object.assign() は Prototype Pollution を防げますか?
完全に安全ではありません。Object.assign() はコピー元の列挙可能な自身のプロパティをコピー先へ代入します。コピー元に列挙可能な自身の __proto__ キーがあると、代入時に継承された setter が呼ばれ、コピー先オブジェクトのプロトタイプが変わる場合があります。浅い代入だけで共有 Object.prototype が変更されるわけではありませんが、再帰的マージは影響を広げる可能性があります。
// A JSON own "__proto__" key reaches the inherited setter const source = JSON.parse('{"__proto__":{"isAdmin":true}}'); const target = Object.assign({}, source); // target's prototype changes; Object.prototype does not. // Recursive merge can reach Object.prototype function deepAssign(target, source) { for (const key of Object.keys(source)) { if (source[key] && typeof source[key] === 'object') { target[key] = target[key] || {}; deepAssign(target[key], source[key]); // With key === '__proto__', target[key] can be // Object.prototype; recursion then writes into it. } else { Object.assign(target, { [key]: source[key] }); } } }
// Spread creates an own data property; it does not invoke the setter. const shallow = { ...userInput }; // Reject dangerous keys at every depth and use own data properties. const blocked = new Set(['__proto__', 'constructor', 'prototype']); function isRecord(value) { if (value === null || typeof value !== 'object' || Array.isArray(value)) return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } function copySafe(value) { if (Array.isArray(value)) return value.map(copySafe); return isRecord(value) ? mergeSafe(value) : value; } function mergeSafe(source, target = Object.create(null)) { if (!isRecord(source)) throw new TypeError('Expected a plain object'); for (const key of Object.keys(source)) { if (blocked.has(key)) continue; const value = source[key]; const current = Object.hasOwn(target, key) ? target[key] : undefined; const next = isRecord(value) ? mergeSafe(value, isRecord(current) ? current : Object.create(null)) : copySafe(value); Object.defineProperty(target, key, { value: next, enumerable: true, writable: true, configurable: true }); } return target; }
Object.assign() が __proto__ キーに対して行うこと
Object.assign() はコピー先の通常のプロパティ設定動作を使って、コピー元の列挙可能な自身のプロパティをコピーします。JSON から解析したコピー元に自身の __proto__ プロパティがあり、コピー先が旧式 setter を継承している場合、その setter がコピー先の [[Prototype]] を変更します。この場合、文字どおりの自身の __proto__ プロパティは作成されません。
オブジェクト spread ({ ...source }) は結果にデータプロパティを作るため、同じキーは自身のデータプロパティになり、継承 setter を呼びません。浅いコピーでは有効な違いですが、後続の再帰的マージ全般を防ぐものではありません。
再帰的マージが共有プロトタイプを汚染する仕組み
安全でない再帰コードが攻撃者制御のキーをたどって共有プロトタイプに書き込むと Prototype Pollution が起こります。よくある実装はネストしたオブジェクトを探すため target[key] を読み取ります。key が __proto__ の場合、その読み取りは Object.prototype を返し、次の再帰処理で通常のオブジェクト全体に影響する可能性があります。constructor.prototype なども確認が必要です。
レビューでは、攻撃者制御のキーを使って対象を読み書きする処理、setter の呼び出し有無、すべての再帰階層で危険なキーを拒否するかを確認します。
よくある質問
{ ...obj }) も同じように安全ですか?浅いコピーでは安全です。spread は自身のデータプロパティを作成し、継承 setter を呼びません。一方、Object.assign() は列挙可能な __proto__ キーを受け取るとコピー先のプロトタイプを変更することがあります。どちらの浅い操作も単独では共有 Object.prototype を変更しません。
structuredClone() は独立したコピーを作り、コピー元のプロトタイプチェーンを適用しません。自身の __proto__ データキーはコピー先でもデータとして残りますが、後続の危険なマージがそのキーを特別扱いする可能性はあります。
保守されている最新のマージライブラリを使い、すべての階層で __proto__、constructor、prototype の扱いを確認してください。広く使われる複数の merge や object-path ユーティリティに Prototype Pollution の勧告が出ています。修正済みバージョンを使ってください。