FlawOpen

flawopen.com/プロトタイプ汚染/JSON.parse()

JSON.parse() はプロトタイプ汚染を起こしますか?

CWE-1321:プロトタイプ汚染リファレンスガイド
結論

いいえ。JSON.parse() は __proto__ という名前の自分自身のデータプロパティを作ります。解析時に継承されたsetterは呼び出されず、プロトタイプも変更されません。後続コードがsetter経由でそのキーをコピーしたり、信頼できないキーを再帰的にマージしたり、攻撃者が制御するオブジェクトパスに書き込んだりすると危険が生じます。

危険なコピー — ローカルな対象のみ
// JSON.parseはデータを作るだけで、プロトタイプを汚染しない。
const input = '{"__proto__":{"isAdmin":true}}';
const parsed = JSON.parse(input);

// Object.assignは対象へのSetを使う。ここでは
// このローカルな例示オブジェクトのプロトタイプだけが変わる。
const copied = Object.assign({}, parsed);

Object.getPrototypeOf(copied).isAdmin; // true
Object.prototype.isAdmin;              // undefined
安全な処理 — 既知の形だけを構築
// 解析後、アプリが許可するフィールドだけを構築する。
const input = '{"theme":"dark","locale":"en","__proto__":{"isAdmin":true}}';
const parsed = JSON.parse(input);
const config = {
  theme:  parsed.theme,
  locale: parsed.locale,
};

// 信頼できないキーの辞書では継承setterを避ける。
const options = Object.assign(Object.create(null), parsed);

JSON.parse()が汚染の発生源ではない理由

JSONのキーとオブジェクトリテラルのプロトタイプsetterは別の操作です。JSON解析は新しい値に自分自身のプロパティを定義し、obj.__proto__ = valueを実行しません。後続操作は異なる動作をします。Object.assign(target, source)は対象へ代入するため、自分自身のキー__proto__が対象の継承setterを呼ぶことがあります。これは対象オブジェクトのプロトタイプを変えますが、それだけでObject.prototypeを変更するわけではありません。

const o = JSON.parse('{"__proto__":{"a":1}}')の後、Object.hasOwn(o, "__proto__")はtrueで、Object.prototype.aは未定義のままです。後続のコピーで通常の対象にこのキーを代入すると、対象のプロトタイプが変わる場合があります。オブジェクトのspreadはsetterを呼ばず自分自身のプロパティを作りますが、危険な再帰的マージやパス書き込みへの防御にはなりません。

実際のリスクが生じる場所

解析済みJSONは信頼できないデータであり、汚染されたプロトタイプではありません。リスクは後続の書き込み箇所によって決まります。再帰的マージ、パスsetter、通常オブジェクトへ代入するコピー処理などです。脆弱な再帰的マージは__proto__やconstructor.prototypeをたどり共有プロトタイプに到達することがあります。一段階のコピーでは対象だけが変わる場合もあります。影響を述べる前に実際の関数と対象を追跡してください。

JSONの解析箇所だけでなく、信頼できないキーを書き込む箇所を調べます。深いマージ、オブジェクトパスsetter、代入を行うfor...inループが対象です。ユーザー制御のキーでは__proto__、constructor、prototypeを拒否し、検証済みの既知形式で再構築するか、プロトタイプを持たない辞書を使います。

よくある質問

常にreviverで__proto__を除去すべきですか?

アプリがそのキーを必要としないなら、reviverで解析時に削除できます。これは多層防御であり、危険なマージやパス書き込みの修正の代わりにはなりません。constructor.prototypeやJSON以外の入力も確認してください。

Expressやbody-parserが守ってくれますか?

本文パーサーはJavaScript値を生成するだけで、後続のコピーやマージを安全にするものではありません。使用するパーサーと後続処理を確認してください。クエリ文字列パーサーによっては角括弧記法から入れ子オブジェクトを作るため、別の入力経路として扱います。

JSON.stringify()はプロトタイプ汚染のリスクですか?

いいえ。シリアライズは値を読み取ってJSON文字列を作るだけで、プロトタイプを変更しません。その文字列を解析して危険な処理に渡すと、リスクが再び生じる可能性があります。

参考資料

関連項目 JavaScriptのプロトタイプ汚染:原因と対策 JSON.parse()はeval()より安全ですか? lodash.merge()はプロトタイプ汚染に安全ですか? Object.assign() と Prototype Pollution:何が安全?