FlawOpen

flawopen.com/脆弱性/JavaScriptのプロトタイプ汚染

JavaScriptのプロトタイプ汚染

深刻度:高 CWE-1321 サプライチェーンとランタイム
やさしい解説 — 共有ルールブック

各機器が自分の設定で答えられないとき、共有のルールブックを参照する作業場を想像してください。係員が攻撃者の指定したルールを書き込むと、別の機器もそれを既定値として使うかもしれません。Prototype Pollutionでは、安全でない動的なプロパティ書き込みがオブジェクトのプロトタイプを変更し、ときには Object.prototype に到達します。影響が自動的に全体へ広がるわけではなく、後続コードが継承プロパティを信頼する場合に問題になります。

対象:信頼できないキーをマージしたり、利用者が制御するプロパティパスを書き込んだりするサーバーまたはブラウザーのJavaScript
侵入経路:__proto__ などの信頼できないキー、または危険な書き込みに至る constructor.prototype パス
影響:到達可能な gadget に依存します。ロジック変更、サービス拒否、XSS、サーバー側の一部ケースでのコード実行などがありますが、自動的に発生するわけではありません
対策:schemaまたはallowlistで検証し、信頼できないキーには Map かプロトタイプを持たない辞書を使います

基本概念とサブシステム用語

Object.prototype
通常のオブジェクトが継承する共有プロトタイプです。汚染されると多くのオブジェクトに影響しますが、すべてのJavaScriptオブジェクトやrealmに及ぶわけではありません。
__proto__
通常のオブジェクトにある旧式のアクセサーで、プロトタイプを取得または設定します。JSONテキストでは、後続コードが危険に扱うまでは同じ表記も通常のプロパティ名です。
再帰的な深いマージ
ネストしたキーをたどり、別のオブジェクトへ書き込む関数です。信頼できないキーがプロトタイプ関連のプロパティや危険なパスを選べると脆弱になります。
CWE-1321
オブジェクトのプロトタイプ属性に対する不適切に制御された変更です。弱点は無制御な変更であり、その後の影響は到達可能なアプリケーション動作によって決まります。

攻撃の流れ(ステップ別)

ステップ 1

信頼できないプロパティ入力

エンドポイントが攻撃者の制御するオブジェクトキーを受け取ります。独自の __proto__ プロパティを含むJSONを解析しただけでは、プロトタイプは変更されません。

ステップ 2

安全でない動的書き込み

再帰的マージやパスsetterがそのキーをたどり、各パス要素を検証せず動的に書き込みます。

ステップ 3

対象または共有プロトタイプの変更

対象と書き込み順序によって、対象オブジェクトのプロトタイプを変える場合と、Object.prototype のような共有プロトタイプへ到達する場合があります。両者は異なります。

ステップ 4

影響はgadget次第

後続のgadgetがセキュリティ上重要な処理で継承値を読み取ります。影響はそのgadget次第であり、認証回避やコード実行を前提にしてはいけません。

仕組みと根本原因

JavaScriptのプロパティ検索はプロトタイプチェーンをたどります。脆弱なマージやパスsetterは、__proto__ や constructor.prototype のような信頼できないキーにプロトタイプへの書き込みを誘導されることがあります。JSON.parse が作る独自の __proto__ フィールドだけでは汚染にならず、後続コードが危険な代入として解釈する必要があります。対象プロトタイプの変更はその対象に影響し、Object.prototype の変更はそれを継承する通常のオブジェクトに影響します。セキュリティ影響には、継承値を使う別のgadgetが必要です。

utils.js(脆弱な再帰マージ)脆弱な例
// 脆弱な例:攻撃者が制御するキーが再帰的な書き込みに到達する
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);
utils.js(allowlistによるプロフィール解析)安全な例
// 安全な例:このエンドポイントが許可するフィールドだけをコピーする
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;
}

攻撃と悪用の手順

防御設計と予防策

よくある質問

JSON.parse() 自体がプロトタイプを汚染しますか?

いいえ。JSON.parse() は __proto__ という自所有データプロパティを作り、継承 setter を呼びません。その後のマージや代入が setter を呼ぶと危険になります。JSON.parse() は Prototype Pollution を起こす?を参照してください。

Object.freeze(Object.prototype) で直りますか?

凍結した Object.prototype 自体への書き込みを防げますが、setter による個別オブジェクトのプロトタイプ変更や他のプロトタイプまでは防げません。組み込みオブジェクトを拡張するライブラリにも影響します。データ処理の修正ではなく多層防御として使ってください。

TypeScript なら安全ですか?

いいえ。型は実行時には消えます。Record<string, string> と宣言した値にも __proto__ キーを含められます。信頼できないキーを検証し、安全にオブジェクトを作成してください。

関連するセキュリティ解説と事例:セキュリティ記事一覧 →