CyberFix Note
セキュアコーディング

プロトタイプ汚染(Prototype Pollution)の仕組みと対策。継承を断ちObject.prototypeを守る

対象の目安: WebフロントエンドやNode.jsを実装する開発者 / 実務

ソウ攻撃・脆弱性リサーチ担当
・ 約17分で読めます
プロトタイプ汚染(Prototype Pollution)の仕組みと対策。継承を断ちObject.prototypeを守る

プロトタイプ汚染(Prototype Pollution)は、JavaScriptのオブジェクトが共有する親であるObject.prototypeを、外部からの入力で書き換えられてしまう欠陥です。JavaScriptのほとんどのオブジェクトはObject.prototypeを継承しているため、この共有された親に一度プロパティが差し込まれると、その後に作られるものを含めてほぼ全てのオブジェクトがその影響を受けます。単体では実害が見えにくい一方で、ほかの欠陥と組み合わさると認可の回避やサービス停止、条件次第では任意コード実行にまで発展しうる、影響範囲の広い問題です。

この記事は、WebフロントエンドとNode.jsのバックエンドを実装する開発者に向けて、なぜ信頼できない入力の扱い方ひとつでObject.prototypeが汚染されるのかを機構レベルで整理し、継承を断つ設計と危険なキーの拒否で塞ぐ方法をまとめます。攻撃の再現手順や動くペイロードは扱わず、仕組みと防御実装に力点を置きます。

注意

本記事は防御実装の解説を目的とし、攻撃の再現手順や動作するペイロードは扱いません。参照先には検証手順を含むものがあります。挙動を確かめる場合は、自分が所有する環境か明示的な許可を得た環境に限り、不正アクセス禁止法などの適用法令や契約、利用規約を守ってください。

プロトタイプ汚染が成立する機構

JavaScriptでは、オブジェクトリテラルや配列などのほとんどのオブジェクトが、Object.prototypeという共通の親を通じてプロパティを継承します。あるオブジェクトに存在しないプロパティを参照すると、実行環境は継承の連鎖(プロトタイプチェーン)をたどって親をさかのぼり、Object.prototypeまで探しにいきます。この仕組みのおかげで、toStringhasOwnPropertyのような共通のメソッドをどのオブジェクトからも呼べます。裏を返せば、Object.prototypeに何かを書き加えると、その追加分は連鎖をたどる全てのオブジェクトから見えるようになります。

問題は、その共有された親へたどり着く経路が、オブジェクトのプロパティ名として表に出ている点にあります。__proto__はオブジェクトの親(内部の[[Prototype]])を読み書きするアクセサで、多くの場合Object.prototypeを指します。同様にconstructorをたどると生成元の関数に届き、そこからprototypeをたどると再び共有された親に到達します。つまり、外部から与えられた名前でオブジェクトのプロパティを入れ子にたどりながら書き込む処理は、__proto__constructorを渡されると、個々のオブジェクトではなく共有された親へ書き込みを届かせてしまいます。一方で、obj["__proto__"] = vのような一段だけの代入は、そのオブジェクト自身の親(内部の[[Prototype]])を差し替えるだけで、共有されたObject.prototypeは汚染しません。たとえばtarget[k1][k2] = vk1__proto__のときは、target["__proto__"]Object.prototypeを指すため、実質的にObject.prototype[k2] = vが起こります。constructor経由でも、constructorからprototypeへと段階的にプロパティをたどって共有された親に到達し、同じことが起こります。標的になりやすいのは全オブジェクトへ影響するObject.prototypeですが、原理としては到達できる任意のプロトタイプオブジェクトが対象になります。

この経路が開くのは、典型的には信頼できない入力を安全でない再帰マージやディープクローン、あるいはドット区切りのパスによるネストしたプロパティ設定で処理するときです。受け取ったキーの名前を検証せずにオブジェクトへ差し込む実装は、__proto__constructorprototypeといった特別なキーもそのまま受け入れ、共有された親に新しいプロパティを生やしたり、既存のプロパティを別の値へ置き換えたりできてしまいます。JSONのパース自体は__proto__を通常のキーとして扱うため、パースした結果をあとからマージする段で汚染が起こります。

MITREのCWE-1321は、上流のコンポーネントから受け取った入力がオブジェクトの属性を初期化または更新するにもかかわらず、オブジェクトプロトタイプの属性への変更を適切に制御しない欠陥をプロトタイプ汚染と定義しています。__proto__constructorprototypeといったプロトタイプへアクセスできる特別な属性を通じて悪用され、ユーザー入力に基づいて属性を設定する処理や、オブジェクトを再帰的にマージまたはクローンする処理でよく生じると説明しています。影響としてはアプリケーションデータの読み書きや、型の異なる値での上書きによる可用性への影響を挙げ、緩和策としてObject.freeze(Object.prototype)、危険なキーの拒否、Object.create(null)やMapの利用、スキーマ検証を示しています。

汚染が実害につながる筋道

Object.prototypeに差し込まれたプロパティそのものが、ただちに攻撃になるわけではありません。実害は、汚染で生えたプロパティが、アプリケーションのどこかで意味のある値として使われたときに現れます。PortSwiggerのWeb Security Academyは、プロトタイプ汚染の悪用が成立するには三つの要素が要ると整理しています。汚染の入口となる攻撃者が制御できる入力(ソース)、汚染されたプロパティが危険な形で使われる箇所(シンク)、そしてソースからシンクへ値を運ぶ橋渡しのプロパティ(ガジェット)です。

この三点がそろうと、汚染は具体的な不具合に化けます。存在しないはずのプロパティが常に存在するように見えることで、条件分岐が意図せず真になり、権限チェックや機能フラグの判定が狂ってロジックや認可の回避につながります。あるいは、内部で使われる設定値やオプションが汚染で上書きされ、型の食い違いから例外が多発してサービスが止まる、といった可用性への影響も起こります。共有された親に残った汚染は、それを触った全てのオブジェクトへ効くため、原因の切り分けが難しくなりがちです。

外部から来た値をどう検証し、どこで信頼するかという設計の土台は、次の記事で整理しています。

あわせて読みたい

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

クライアント側とサーバ側の違い

プロトタイプ汚染は、動く場所によって行き着く先が変わります。ブラウザで動くクライアント側では、汚染されたプロパティがDOMを操作するAPIやスクリプトの挿入経路に流れ込むと、DOMベースのXSSに至ることがあります。汚染で生えたプロパティが、本来は指定されていないはずのHTMLの断片やスクリプトのソースとして使われ、ページ内で意図しないスクリプトが動く筋道です。DOMベースのXSSそのものの仕組みは、次の記事で扱っています。

あわせて読みたい

XSS(クロスサイトスクリプティング)の仕組みと対策。反射型・格納型・DOM型を原理から整理

一方、Node.jsで動くサーバ側のプロトタイプ汚染は、表からは見えにくいものの、条件によってはより重い結果を招きます。PortSwiggerは、サーバ側では汚染されたプロパティが子プロセスを起動する処理などのシンクに届くと、任意コード実行につながりうると説明しています。汚染がリクエストの処理をまたいでプロセス全体に残るため、ある利用者の入力が別の処理の挙動を変えるという、状態の共有ならではの厄介さも伴います。

PortSwiggerのWeb Security Academyは、プロトタイプ汚染を、攻撃者がグローバルなオブジェクトプロトタイプへ任意のプロパティを追加でき、それがユーザー定義のオブジェクトに継承されうるJavaScriptの脆弱性と定義しています。悪用にはソース、シンク、ガジェットの三要素が必要で、クライアント側では主にDOMベースのXSSに、サーバ側ではNode.jsの脆弱なメソッドを介して任意コード実行やサービス拒否に至りうると整理しています。サーバ側は検出が難しい一方でリスクは大きいとしています。

継承を断つ設計と危険キーの拒否

対策の根本は、信頼できないキーがプロトタイプへ到達して書き込むのを許さない、という一点にあります。具体的な手立ては二つです。ひとつは、辞書用途の入れ物に、キー名がプロトタイプチェーン上のプロパティにならない構造を使い、汚染の届く先をなくすことです。もうひとつは、共有された親へたどり着くキーを入口で拒否し、共有された親そのものを書き換え不能にすることです。OWASPのPrototype Pollution Prevention Cheat Sheetは、この方向に沿った具体策を挙げています。

目的危うい実装汚染を断つ手立て
任意のキーを持つ辞書オブジェクトリテラル {} を入れ物に使うnew Map()Object.create(null) を入れ物に使い、キー名をプロパティに載せない
外部入力のマージや展開キー名を検証せず再帰マージやディープクローンする__proto__constructorprototype を拒否し、許可リストでキー名を検証する
共有された親の保護既定のまま書き換え可能にしておくObject.freeze(Object.prototype) で共有された親を凍結する(依存先の確認は必要)
ランタイムでの遮断__proto__ 経由の到達を放置するNode.js の --disable-proto=delete__proto__ を削除する
構造の検証受け取ったJSONをそのまま信頼して展開するJSON Schema などで想定した構造とキーだけを通す

継承を断つ考え方が最も素直に効くのはObject.create(null)です。プロトタイプがnullの素のオブジェクトなのでObject.prototypeを継承せず、__proto__というキーも共有された親への近道ではなく、ただの文字列のプロパティになります。Mapはしくみが異なります。Mapインスタンス自体はMap.prototypeを介してObject.prototypeを継承しますが、set()get()で渡したキーはMap内部のエントリとして保持され、オブジェクトのプロパティ名にはなりません。そのため__proto__のようなキー名を渡してもプロトタイプチェーン上のプロパティにはならず、汚染の経路になりません。ユーザー入力をキーとして格納する入れ物には、オブジェクトリテラルではなくObject.create(null)Mapを使うのが確実です。

危険なキーの拒否は、汚染への経路を入口で断ちます。再帰マージやプロパティ設定を自作するなら、キー名が__proto__constructorprototypeのいずれかに一致する場合は処理をスキップまたは拒否し、できれば想定するキーの許可リストで検証します。既存のライブラリに頼る場合は、プロトタイプ汚染への対策が入った保守されているバージョンを使い、脆弱性情報を追える状態にしておきます。

利用しているライブラリに既知のプロトタイプ汚染が残っていないかを継続的に把握する仕組みは、次の記事で扱っています。

あわせて読みたい

依存ライブラリ管理とSCA。SBOMで攻撃面を把握する

Object.freeze(Object.prototype)は、共有された親を凍結して新しいプロパティの追加や既存プロパティの変更を拒むため、たとえ危険なキーが差し込まれても書き換えが成立しなくなります。効かせるには、信頼できない入力を処理し始めるより前、できるだけ早い起動段階で適用します。守れるのはObject.prototypeだけで、ほかのプロトタイプや代入先自身の親([[Prototype]])の差し替えは防げないため、危険なキーの拒否を置き換えるものではなく、多層防御の一枚として重ねます。組み込みのプロトタイプを書き換えて動くライブラリがあると壊れるため、導入前に依存関係の挙動を確認します。Node.jsでは、起動時に--disable-proto=deleteを渡すとObject.prototype.__proto__というアクセサ自体が取り除かれ、throwを指定すればアクセス時にERR_PROTO_ACCESSという例外を投げます。これは__proto__という経路を実行環境の側で塞ぐ、多層防御の一枚として働きます。

OWASPのPrototype Pollution Prevention Cheat Sheetは、オブジェクトリテラルの代わりにnew Set()new Map()を使うことを勧めています(値の集合にはSet、キーと値の対応にはMap)。オブジェクトが必要な場合はObject.create(null)Object.prototypeを継承しないように作るとしています。組み込みのプロトタイプの変更を防ぐためにObject.freeze()Object.seal()を使えるとしつつ、プロトタイプを書き換えるライブラリがあるとアプリケーションが壊れうると注意しています。さらにNode.jsでは--disable-proto=delete__proto__プロパティを取り除けると紹介しています。

  1. 1

    ユーザー入力をキーにする箇所を洗い出す

    外部から来た値を、そのままオブジェクトのプロパティ名として使っている箇所を探します。設定のマージ、クエリやフォームの展開、ドット区切りパスによるネストした値の設定、ディープクローンなどが対象です。

  2. 2

    キーバリューの入れ物をMapやObject.create(null)に変える

    ユーザー入力をキーとして格納する入れ物は、オブジェクトリテラルではなくMapObject.create(null)に置き換えます。これで__proto__というキーが共有された親への近道でなくなります。

  3. 3

    マージやプロパティ設定で危険キーを拒否する

    再帰マージやプロパティ設定を自作している場合は、__proto__constructorprototypeのキーをスキップまたは拒否し、可能なら想定するキーの許可リストで検証します。受け取るデータの構造は、JSON Schemaなどで先に検証します。

  4. 4

    共有された親とランタイムを固める

    Object.freeze(Object.prototype)で共有された親を凍結できるか、依存ライブラリの挙動を確認したうえで検討します。Node.jsでは起動オプションに--disable-proto=deleteを加え、__proto__という経路を実行環境の側でも塞ぎます。

  5. 5

    依存ライブラリを保守された版に保つ

    マージやクローン、パスによる代入を提供するライブラリは、プロトタイプ汚染への修正が入った版を使い、脆弱性情報を追える状態を保ちます。自作せず、保守されたライブラリに寄せるのも有効な選択です。

信頼できないデータを構造ごと復元する処理は、プロトタイプ汚染と同じく入力の扱いに注意が要ります。安全でないデシリアライゼーションの考え方は、次の記事とあわせて読むと土台が固まります。

あわせて読みたい

安全でないデシリアライゼーションの対策。信頼できないデータの復元が任意コード実行につながる仕組み

まとめ

プロトタイプ汚染(CWE-1321)は、JavaScriptのオブジェクトが共有する親であるObject.prototypeを、外部からの入力で書き換えられてしまう欠陥です。信頼できない入力を安全でない再帰マージやプロパティ設定で処理し、__proto__constructorprototypeというキーを許すと、共有された親が汚染され、以降ほぼ全てのオブジェクトへ影響が波及します。単体では悪用しにくくても、ほかの欠陥と連鎖すると、クライアント側ではDOMベースのXSS、サーバ側では条件次第で任意コード実行やサービス停止にまで発展しうる点が、この問題の広がりです。

対策の軸は、辞書用途の入れ物でキー名をプロトタイプチェーン上のプロパティにしないことと、共有された親へたどり着くキーを入口で拒否し、共有された親そのものを書き換え不能にすることです。Object.create(null)Mapの利用、危険なキーの拒否と構造の検証、Object.freeze(Object.prototype)やNode.jsの--disable-proto、保守されたライブラリへの依存を重ねれば、単一の対策に頼らずに汚染の経路を塞げます。

プロトタイプ汚染対策で確認したいポイント

  • 外部入力をそのままオブジェクトのプロパティ名として使っている箇所を洗い出したか
  • ユーザー入力をキーにする入れ物を、オブジェクトリテラルではなく Map や Object.create(null) にしているか
  • 自作の再帰マージやプロパティ設定で、__proto__、constructor、prototype のキーを拒否しているか
  • 受け取るデータの構造を JSON Schema などの許可リストで検証しているか
  • 依存ライブラリの挙動を確認したうえで、Object.freeze(Object.prototype) の導入を検討したか
  • Node.js で --disable-proto=delete などのランタイム対策を多層防御として加えたか
  • マージやクローンを提供するライブラリを、プロトタイプ汚染への修正が入った保守版に保っているか

Webアプリの代表的な弱点を全体像として押さえたい場合は、次の記事が入口になります。

あわせて読みたい

OWASP Top 10とは。開発者が押さえるべきWebの代表的リスクと対策を一気に理解する

出典・参考

この記事をシェア

関連する記事

セキュアコーディング

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

セキュアコーディングの土台である入力バリデーションと出力エスケープを、それぞれの役割の違いから整理します。入口の検証は防御の一段目、出口の文脈別エスケープがXSSやインジェクションの根本対策である理由を、原理と実務の判断基準まで掘り下げます。

セキュアコーディング

安全でないデシリアライゼーションの対策。信頼できないデータの復元が任意コード実行につながる仕組み

安全でないデシリアライゼーション(CWE-502)を、発生原理から対策まで開発者目線で整理します。オブジェクトの復元がなぜ任意コード実行やDoSにつながるのか、ガジェットチェーンの考え方、OWASP Top 10での位置づけの変遷、そしてネイティブ形式を避けJSONを使う根本対策と言語別の危険APIの避け方を解説します。