何が起きるか

JavaScriptのオブジェクトは、自身にないプロパティをプロトタイプという共有元から探します。プロトタイプ汚染は、攻撃者の入力を処理するコードがこの共有元を書き換え、他のオブジェクトが読む値まで変えてしまう問題です。

説明用に、利用者から受け取った入れ子のJSONを設定へ再帰的にマージする機能を考えます。__proto__などのキーをたどる処理が、通常の設定ではなくObject.prototypeという共有元へ到達し、そこへisAdmin=trueを入れるとします。user自身にisAdminがない場合、user.isAdminの参照は共有元のtrueを返します。APIがその値だけを管理者判定に使えば、攻撃者によるユーザー削除を許可するおそれがあります。 具体的に入力が{"__proto__":{"isAdmin":true}}で、マージ処理がtarget[key]を読みながら入れ子へ進むとします。通常のオブジェクトのtarget["__proto__"]から共有元を得て、そこでisAdminへtrueを代入する実装なら、通常の設定キーの保存にとどまらず共有元を書き換えます。

JSONを受け取るだけで必ず共有元が変わるわけではありません。特別なキーをたどって共有元へ代入する処理と、汚染された値を重要な判断へ採用する処理の両方が必要です。 ここではサーバー内の一つのJavaScript実行環境を仮定しています。同じ共有元を継承する別の利用者のオブジェクトにも値が見える場合がありますが、別のブラウザや別サーバーのメモリまで自動で書き換えるものではありません。

正常ならどう動くか

設定更新APIは、受け取ったJSONから許可する名前・型の値だけを取り出し、その利用者の設定へ保存します。入れ子の値をコピーするときも、キーをたどって共有のプロトタイプへ代入しないマージ実装を使います。

管理者かどうかなどの重要情報は、利用者が更新できる設定とは別に管理します。プロパティを検査する場合は、Object.hasOwnでオブジェクト自身が持つ値か確認し、未指定の値を共有元から見つけただけで許可しません。

不備と攻撃が成立する流れ

攻撃者が__proto__などの特別なキーを含む入れ子のJSONを、設定更新機能へ送る例です。再帰的なマージがキーを単なる設定名として検査せず、Object.prototypeなど共有元への経路としてたどると、利用者用の設定ではなく共有のプロトタイプへ値を代入します。 再帰的なマージは、入れ子のキーごとにコピー先の同名プロパティへ進んで値を写す処理です。入力のキーが__proto__のときに、コピー先の継承関係までたどってしまう実装では、最後の値をObject.prototypeへ代入してしまいます。

別のオブジェクトで未指定の設定を参照すると、そのオブジェクト自身に値がなくても共有元の値が返ります。説明用に、共有元へisAdmin=trueが入り、user自身にはisAdminがない場合を考えます。認可がuser.isAdminだけで判定すると、継承したtrueを使い、その利用者を管理者として扱うおそれがあります。

起こり得る被害と条件

認可迂回の例では、isAdminの継承値を管理権限として採用したAPIが、本来禁止されたユーザー削除などを許可します。攻撃者は他人のアカウントを操作できるおそれがあります。プロトタイプの書換経路と、その値を認可に使う処理の両方が必要です。

設定変更の例では、未指定の通信先を継承値から補う処理が、攻撃者のURLを採用します。そこへ設定や解析対象を送信する構成なら、攻撃者は自分のサーバーで送信データを入手できます。

DOM XSSの別の説明例では、ブラウザ内のプロトタイプへ表示用HTMLの文字列が入り、アプリが未指定のhtml設定としてその継承値を読み、innerHTMLへ渡すとします。値がimg要素とonerror属性を含み、画像の読み込みが失敗すると、属性内のJavaScriptが動く構成があります。そのコードがページの非公開メッセージを読み外部へ送れれば漏えいします。サーバー内の汚染だけでブラウザのDOMが変わるのではなく、ブラウザ側でその値を採用する経路も条件です。

サーバーでコード実行へ至る例では、継承値がプロセス起動の設定などへ採用され、攻撃者が指定する処理が起動されます。どの値がどの危険な機能に渡るかという追加の経路が必要です。共有元を変えられるだけで、これら全ての被害が成立するわけではありません。

防ぎ方と、防げる理由

入力のキーと型をスキーマで限定し、__proto__やconstructor.prototypeなどの経路から共有元へ到達しないマージ実装を使います。単に__proto__という一語だけを除いても、別の経路で同じ共有元へ届く実装では不十分です。 constructor.prototypeは、この例では{"constructor":{"prototype":{"isAdmin":true}}}という入れ子の経路です。マージがコピー先のconstructorを読み、さらにprototypeへ進むと同じ共有元へ届く場合があるため、キーの経路全体を検査します。

辞書として値を保存する用途ではMapや、プロトタイプを持たないオブジェクトを検討します。これにより、通常のプロトタイプの継承を値の検索へ混ぜる経路を減らせます。重要な設定はObject.hasOwnで自身のプロパティかを確認し、共有元から継承した値だけで認可や通信先を決めません。共有元の書き換えと、誤った値の採用の両方へ対策します。

よくある誤解

__proto__だけを拒否しても、constructor.prototypeなど別経路が残る場合があります。一方、JSONとして解析しただけで常に共有元が変わるわけでもありません。実際にどの処理がキーをたどって代入するかが重要です。

実装の確認点

深いマージ、URLの入れ子パラメータ解析、設定の既定値参照を確認します。入力がどの共有元へ届くかに加え、汚染したプロパティをどこで読むかまで追い、被害の成立を判断します。 たとえばsettings[theme]=darkを{settings:{theme:"dark"}}へ変換するURL解析があります。同じ変換で攻撃者が入れ子のキーを自由に選べ、その結果を危険なマージへ渡すと、JSONを直接受ける例と同じ代入先へ到達する場合があります。

出典・確認範囲

  1. [1] Web Security Academy: プロトタイプ汚染PortSwigger / 確認日 2026-10-29