プロトタイプ汚染とは?起きるパターンと対策法

プロトタイプ汚染とは?起きるパターンと対策法

#セキュリティ#Web開発#脆弱性対策

プロトタイプ汚染は、JavaScript特有の仕組みを突いて、通常の多くのオブジェクトが継承する性質を書き換える脆弱性です。 対策の勘所は、外部入力のキーをそのままオブジェクトへ反映させず、危険なキーを防げているかどうかです。

プロトタイプ汚染とは

プロトタイプ汚染(prototype pollution):JavaScriptで__proto__などの特殊なキーを経由してObject.prototypeなどを書き換え、そのプロトタイプを継承するオブジェクトの既定の性質を汚染する脆弱性です。 外部入力のキーを検証せずにオブジェクトへ代入することが根本原因になります。

Object.prototypeは通常の多くのオブジェクトが継承するため、ここを書き換えると影響がアプリ全体に広がる可能性があります。 ただし、Object.create(null)で作ったオブジェクトや別のプロトタイプチェーンを持つ値には、そのまま影響するわけではありません。 既存プロパティの上書きによる挙動の改変、汚染した値をきっかけにしたDoS、条件次第では他の脆弱性と組み合わさりコード実行に至ります。

プロトタイプ汚染が起きるパターン

典型的な弱点は次のとおりです。

  • 外部から受け取ったオブジェクトを再帰的にマージする処理で、__proto__constructorのキーを弾いていない
  • クエリ文字列やJSONをネストしたオブジェクトに変換する際、特殊なキーをそのまま反映している
  • 設定オブジェクトのディープコピーやデフォルト値の適用で、キーの検証をしていない
  • 動的にプロパティ名を組み立てて代入する処理に、外部入力のキーが流れ込む
  • 汚染されたObject.prototypeのプロパティが、後続のロジックで既定値として参照される
  • 上記の処理を持つ古いライブラリを更新せずに使っている

共通するのは「外部入力のキーがそのままオブジェクトの構造に反映され、特殊なキーを防げていない」という点です。

対策法

原則は「外部入力のキーを信用せず、プロトタイプに触れる代入を遮断する」ことです。

  • マージやコピーの処理で__proto__constructorprototypeといったキーを拒否する。これが本命の対策になる
  • キーと値の入れ物にはObject.create(null)やMapを使い、プロトタイプを持たせない
  • 外部入力から作るオブジェクトのキーを、許可リストやスキーマで検証する
  • マージやディープコピーは、プロトタイプ汚染対策済みの実績あるライブラリを使う
  • ライブラリを新しいバージョンに保ち、既知の汚染経路をふさぐ

自前の再帰マージは危険なキーを見落としやすく、本命はキーの遮断と検証済みライブラリの利用です。 外部入力をオブジェクトに展開する箇所を洗い出し、危険なキーの拒否から入れるのが費用対効果の高い進め方になります。

React(TypeScript)での事例

プロトタイプ汚染は JavaScript 固有の脆弱性です。 Ruby にはプロトタイプチェーンがないため Rails 側では発生せず、React アプリ本体と Node 側(SSR や BFF)が対象になります。

危険なのは、外部入力を再帰的にマージする自前実装です。

// 危険:__proto__ キーをそのまま辿ってマージしてしまう
function deepMerge(target: Record<string, any>, source: Record<string, any>) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === "object" && source[key] !== null) {
      target[key] ??= {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

deepMerge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log(({} as any).isAdmin); // true。Object.prototypeを継承する無関係なオブジェクトにも影響する

JSON.parse 自体は安全で、__proto__ をただのキー名として持つオブジェクトを返すだけです。 汚染が起きるのは、そのキーをマージ処理が辿った瞬間です(target["__proto__"] へのアクセスが Object.prototype を返し、そこへ値が書き込まれます)。

直すには、危険なキーをマージの入り口で拒否します。

const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);

function isPlainObject(value: unknown): value is Record<string, unknown> {
  if (value === null || typeof value !== "object") return false;
  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

function deepMerge(
  target: Record<string, unknown>,
  source: Record<string, unknown>,
) {
  for (const key of Object.keys(source)) {
    if (FORBIDDEN_KEYS.has(key)) continue;

    const sourceValue = source[key];
    if (isPlainObject(sourceValue)) {
      const next: Record<string, unknown> = Object.create(null);
      if (Object.hasOwn(target, key) && isPlainObject(target[key])) {
        Object.assign(next, target[key]);
      }
      target[key] = deepMerge(next, sourceValue);
    } else {
      target[key] = sourceValue;
    }
  }
  return target;
}

継承プロパティを再帰先に使うと、toStringのようなキーを通じて共有オブジェクトを書き換える余地が残ります。 Object.hasOwnで自分自身のプロパティだけを対象にし、再帰先には新しいnull-prototypeオブジェクトを使います。 この例では配列を再帰的にマージせず値として置き換える方針にし、マージ対象をplain objectに限定しています。 キーと値の入れ物として使うオブジェクトは Object.create(null)Map にすると、汚染の書き込み先自体がなくなります。 実務では自前マージを避け、既知のプロトタイプ汚染が修正されたバージョンのライブラリを使うことで、見落としのリスクを下げられます。 ただし、最新版であることだけでは未知の脆弱性や誤った使い方まで防げないため、入力スキーマによるキーの制限も併用します。 過去には広く使われるユーティリティライブラリにも汚染経路の修正例があるため、既知の問題を検出するnpm auditと依存の更新を対策の一部として組み込みます。

まとめ

プロトタイプ汚染対策の要点を整理します。

  • 根本原因は「外部入力のキーがそのままオブジェクトの構造に反映されること」。 Object.prototypeは通常の多くのオブジェクトが継承するため、汚染の影響がアプリ全体に広がる可能性がある。 ただし、Object.create(null)で作ったオブジェクトなどは例外になる
  • 本命の対策は危険なキーの遮断。マージやコピーの入り口で__proto__constructorprototypeを拒否する
  • JSON.parse自体は安全で、汚染が起きるのはそのキーをマージ処理が辿った瞬間。target["__proto__"]へのアクセスがObject.prototypeを返し、そこへ書き込まれる
  • キーと値の入れ物にはObject.create(null)Mapを使う。汚染の書き込み先そのものをなくせる
  • 自前の再帰マージを避け、既知の問題が修正されたライブラリを使う。最新版だけに依存せず、入力スキーマによる制限とnpm auditも組み合わせる

JavaScript固有の脆弱性であり、RubyにはプロトタイプチェーンがないためRails側では同じ仕組みの問題は起きません。 ブラウザで動くReactアプリだけでなく、SSRやBFFなどのNode側も対象になるため、JavaScriptで外部入力をオブジェクトへ展開している箇所を洗い出してください。

関連して読める記事

「外部から渡されたキーを無選別に受け入れる」という入力処理は、サーバ側のマスアサインメントにも見られる共通点です。 ただし、プロトタイプ汚染はプロトタイプチェーンを書き換える問題、マスアサインメントは許可していないモデル属性まで更新する問題であり、同じ脆弱性ではありません。 プロトタイプ汚染はブラウザとNodeの両方で起こり得ますが、ブラウザ側で別の攻撃につながる場合はXSSとも守備範囲が重なります。 依存ライブラリの更新が対策の一部になる点はReDoSと共通しています。

マスアサインメントとは?起きるパターンと対策法

マスアサインメントの仕組みと起きるパターン、Strong Parameters による許可リスト化を軸にした対策を整理します。permit! で全属性を許可する形と、JSON.parse が返す素の Hash が Strong Parameters の保護対象外になる見落としがちな経路、role のような内部属性を許可リストから外す設計を具体的なコードで示します。

2026年7月25日

XSS(クロスサイトスクリプティング)とは?起きるパターンと対策法

XSS(クロスサイトスクリプティング)の仕組みと3つの型、混入しやすいパターン、そして出力時のエスケープを軸にした対策を整理します。React(TypeScript)の dangerouslySetInnerHTML と Rails(ERB)の raw / html_safe を題材に、危険なコードと安全な書き方を具体的なコード例で示します。

2026年7月22日

ReDoSとは?起きるパターンと対策法

ReDoS(正規表現によるDoS)の仕組みと起きるパターン、破滅的なバックトラッキングを避ける書き換えと入力長の制限を軸にした対策を整理します。入れ子の繰り返しパターンの危険例、Rails の length バリデーションが防御にならない理由、Ruby 3.2 の Regexp.timeout の位置づけを具体的なコードで示します。

2026年7月25日
この記事をシェア