flawopen.com/脆弱性/JavaScriptのプロトタイプ汚染
JavaScriptのプロトタイプ汚染
各機器が自分の設定で答えられないとき、共有のルールブックを参照する作業場を想像してください。係員が攻撃者の指定したルールを書き込むと、別の機器もそれを既定値として使うかもしれません。Prototype Pollutionでは、安全でない動的なプロパティ書き込みがオブジェクトのプロトタイプを変更し、ときには Object.prototype に到達します。影響が自動的に全体へ広がるわけではなく、後続コードが継承プロパティを信頼する場合に問題になります。
__proto__ などの信頼できないキー、または危険な書き込みに至る constructor.prototype パスMap かプロトタイプを持たない辞書を使います基本概念とサブシステム用語
- Object.prototype
- 通常のオブジェクトが継承する共有プロトタイプです。汚染されると多くのオブジェクトに影響しますが、すべてのJavaScriptオブジェクトやrealmに及ぶわけではありません。
- __proto__
- 通常のオブジェクトにある旧式のアクセサーで、プロトタイプを取得または設定します。JSONテキストでは、後続コードが危険に扱うまでは同じ表記も通常のプロパティ名です。
- 再帰的な深いマージ
- ネストしたキーをたどり、別のオブジェクトへ書き込む関数です。信頼できないキーがプロトタイプ関連のプロパティや危険なパスを選べると脆弱になります。
- CWE-1321
- オブジェクトのプロトタイプ属性に対する不適切に制御された変更です。弱点は無制御な変更であり、その後の影響は到達可能なアプリケーション動作によって決まります。
攻撃の流れ(ステップ別)
信頼できないプロパティ入力
エンドポイントが攻撃者の制御するオブジェクトキーを受け取ります。独自の __proto__ プロパティを含むJSONを解析しただけでは、プロトタイプは変更されません。
安全でない動的書き込み
再帰的マージやパスsetterがそのキーをたどり、各パス要素を検証せず動的に書き込みます。
対象または共有プロトタイプの変更
対象と書き込み順序によって、対象オブジェクトのプロトタイプを変える場合と、Object.prototype のような共有プロトタイプへ到達する場合があります。両者は異なります。
影響はgadget次第
後続のgadgetがセキュリティ上重要な処理で継承値を読み取ります。影響はそのgadget次第であり、認証回避やコード実行を前提にしてはいけません。
仕組みと根本原因
JavaScriptのプロパティ検索はプロトタイプチェーンをたどります。脆弱なマージやパスsetterは、__proto__ や constructor.prototype のような信頼できないキーにプロトタイプへの書き込みを誘導されることがあります。JSON.parse が作る独自の __proto__ フィールドだけでは汚染にならず、後続コードが危険な代入として解釈する必要があります。対象プロトタイプの変更はその対象に影響し、Object.prototype の変更はそれを継承する通常のオブジェクトに影響します。セキュリティ影響には、継承値を使う別のgadgetが必要です。
// 脆弱な例:攻撃者が制御するキーが再帰的な書き込みに到達する
function merge(target, source) {
for (const key in source) {
const value = source[key];
if (value && typeof value === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], value);
} else {
target[key] = value;
}
}
return target;
}
// JSON.parse は独自の __proto__ データプロパティを作成する。危険なのは安全でないマージ。
const input = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, input);
// 安全な例:このエンドポイントが許可するフィールドだけをコピーする
const PROFILE_KEYS = new Set(['displayName', 'theme']);
function parseProfile(input) {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new TypeError('profile must be an object');
}
const profile = Object.create(null);
for (const key of Object.keys(input)) {
if (!PROFILE_KEYS.has(key)) continue;
const value = input[key];
if (typeof value !== 'string' || value.length > 80) {
throw new TypeError('invalid profile field');
}
profile[key] = value;
}
return profile;
}
攻撃と悪用の手順
- 1入力: リクエストに
{"__proto__": {"admin": true}}のような予期しないキーが含まれます。JSONの解析だけではプロトタイプは変更されません。 - 2危険な書き込み: 汎用マージやパスsetterがallowlistなしでそのキーを処理します。
- 3プロトタイプへの到達: コード経路によって、書き込みが対象プロトタイプを変えるか、共有プロトタイプに到達します。
- 4影響を生むgadget: 後続コードが機密判断で継承プロパティを信頼する場合にのみ影響します。実際のgadgetと認可経路をテストしてください。
防御設計と予防策
- ✓schema allowlistを優先: エンドポイントで許可するフィールドだけから対象を作成します。汎用パス書き込みが避けられない場合は、各階層でプロトタイプ関連のパス要素を拒否します。
- ✓安全な辞書を使用: 任意の利用者キーは
MapまたはObject.create(null)の辞書へ格納し、通常オブジェクトを期待するAPIへ渡す前に検証します。 - ✓独自プロパティを確認: セキュリティ上重要な有無の確認には
Object.hasOwn(record, key)を使い、継承された既定値で操作を許可しないでください。 - ✓ランタイム強化は多層防御として使う: Node.jsの
--disable-proto=throwは一つの経路を除きますが、constructor.prototypeは防ぎません。組み込みオブジェクトのfreezeはpolyfillや依存関係を壊す可能性があるため互換性を評価し、安全な書き込みの代わりにしないでください。
よくある質問
いいえ。JSON.parse() は __proto__ という自所有データプロパティを作り、継承 setter を呼びません。その後のマージや代入が setter を呼ぶと危険になります。JSON.parse() は Prototype Pollution を起こす?を参照してください。
凍結した Object.prototype 自体への書き込みを防げますが、setter による個別オブジェクトのプロトタイプ変更や他のプロトタイプまでは防げません。組み込みオブジェクトを拡張するライブラリにも影響します。データ処理の修正ではなく多層防御として使ってください。
いいえ。型は実行時には消えます。Record<string, string> と宣言した値にも __proto__ キーを含められます。信頼できないキーを検証し、安全にオブジェクトを作成してください。
- MDN — JavaScriptのプロトタイプ汚染
- OWASP — プロトタイプ汚染の防止ガイド
- CWE-1321 — オブジェクトのプロトタイプ属性に対する不適切に制御された変更
- MDN — Object.prototype.__proto__ アクセサー