【入門編】プロトタイプ汚染(Prototype Pollution)のメカニズムと防御的プログラミング – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!JavaScriptのコアな仕組みへようこそ。
今回は、フロントエンドからNode.jsのバックエンド開発まで避けて通れない、しかし初学者の方が最もハマりやすく、かつセキュリティ事故に直結する恐ろしいテーマ「プロトタイプ汚染(Prototype Pollution)」についてお話ししていきますね。

「オブジェクトの継承関係? なんか難しそう……」と思ったかもしれませんが、安心してください。ここをクリアすれば、JavaScriptのオブジェクトの裏側で何が起きているのかが手に取るように分かり、ワンランク上の安全なコードを書けるようになりますよ。

それでは、V8エンジンのメモリ空間の裏側を覗きながら、一緒に紐解いていきましょう!

—

1. JavaScriptの「継承」とプロトタイプの正体

JavaScriptは、JavaやC++のような「クラスベース」の言語とは異なり、「プロトタイプベース」という独自の仕組みでオブジェクト同士の関係を作っています。

すべてのオブジェクトは、親にあたるオブジェクト(これがプロトタイプです)への隠しポインタ(`[[Prototype]]`、コード上では `__proto__`)を持っています。子どもが見当たづらいプロパティを探しにいくとき、親のプロトタイプまで探しに行く旅に出る。これをプロトタイプチェーンと呼びます。

[ あなたの書いたオブジェクト ]
└─ name: “Taro”
└─ __proto__ (親の設計図)
└─ toString() などのメソッド
└─ __proto__ (さらにその親)
└─ Object.prototype (すべてのオブジェクトの頂点)
└─ __proto__ -> null (ここで終点)

この「すべてのオブジェクトの頂点」に君臨するのが `Object.prototype` です。ここに特別なプロパティを生やすと、JavaScriptの世界に存在するすべてのオブジェクトがその影響を受けてしまうという特徴があります。これが、今回のテーマの鍵になります。

—

2. プロトタイプ汚染とは何か?(脆弱性のメカニズム)

プロトタイプ汚染とは、悪意ある(あるいは不注意な)コードによって、この頂点である `Object.prototype` が書き換えられてしまい、アプリ全体が予期せぬ挙動やセキュリティ脆弱性を引き起こす現象のことです。

言葉だけだとピンとこないと思うので、まずは危険なコードの例を見てみましょう。

// 【危険なコード例】外部から受け取ったデータをそのまま深い階層で代入する処理
function setDeepProperty(obj, path, value) {
let current = obj;
for (let i = 0; i < path.length - 1; i++) { const key = path[i]; if (!current[key]) { current[key] = {}; } current = current[key]; } current[path[path.length - 1]] = value; } const userObj = {}; // もし悪意あるユーザーが、以下のようなパスと値を送り込んできたら…? // パスに "__proto__" と "isAdmin" を指定する const maliciousPath = ["__proto__", "isAdmin"]; setDeepProperty(userObj, maliciousPath, true); // おっと! userObj 自身には "isAdmin" は設定されていないはずなのに… console.log(userObj.isAdmin); // => 出力: true (!?)

// なんと、まったく関係ない「新しく作った空のオブジェクト」まで汚染されている!
const anotherObj = {};
console.log(anotherObj.isAdmin); // => 出力: true (大変だ!)

何が起きたか分かりますか?
`userObj.__proto__` を辿ることで、すべてのオブジェクトの親である `Object.prototype` に `isAdmin: true` が書き込まれてしまいました。その結果、プログラム内の至る所で作られる「普通のオブジェクト」までが、管理者権限を持っているかのようなバグを引き起こしてしまったのです。

これが、サーバーサイド(Node.jsなど)で起きた場合の被害を想像してみてください。ユーザー認証の判定がすべてすり抜けてしまう、極めて危険な脆弱性になり得ますよね。

—

3. 陥りがちな罠:JSONパースと再帰的マージ処理

実務で最もこのプロトタイプ汚染を踏み踏みしやすいのが、「オブジェクトの深層マージ(Deep Merge)」や、外部から受け取ったJSON文字列のパース処理です。

例えば、ユーザーからの設定情報を上書き保存する際によくある、次のような再帰関数を書いていないでしょうか?

// よくある甘い実装の深層マージ関数
function unsafeDeepMerge(target, source) {
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === ‘object’) {
if (!target[key]) target[key] = {};
unsafeDeepMerge(target[key], source[key]); // 再帰的にマージ
} else {
target[key] = source[key];
}
}
return target;
}

// 攻撃者がJSON経由で以下のようなデータを送り込んできたとします
const evilJson = ‘{“__proto__”: {“polluted”: “危険な値だよ!”}}’;
const payload = JSON.parse(evilJson);

const appConfig = {};

// マージを実行してしまうと…
unsafeDeepMerge(appConfig, payload);

// すべてのオブジェクトが汚染されます
const safeObj = {};
console.log(safeObj.polluted); // => “危険な値だよ!”

JSONのパース自体は安全に見えますが、その中身(キー名)をろくに検証せずにオブジェクトのプロパティとして代入・展開していくと、JavaScriptのエンジンは `__proto__` を特別なアクセサ(ゲッター/セッター)として解釈し、プロトタイプチェーンを書き換えてしまうのです。

—

4. 防御的プログラミング:安全なコードを書くための3つの鉄則

「じゃあ、どうやって防げばいいの?」という話ですよね。
ここをクリアすれば、あなたもセキュアなコードを書ける一流エンジニアの仲間入りです。以下の3つの防衛策をしっかりと心に刻んでおきましょう。

鉄則1: ブラックリスト方式で `__proto__` や `constructor` を弾く

オブジェクトのキーを走査する際、危険なキー名が含まれていないかを厳密にチェック(サニタイズ)します。

function isForbiddenKey(key) {
return key === ‘__proto__’ || key === ‘constructor’ || key === ‘prototype’;
}

鉄則2: プロトタイプを持たない「純粋なオブジェクト」を作る

データの入れ物(辞書型データなど)としてオブジェクトを使う場合、最初から `Object.prototype` を継承しないオブジェクトを作るのが最も確実です。
それには、`Object.create(null)` を使います。

// プロトタイプを持たない純粋なオブジェクト(ハッシュマップ)の生成
const pureMap = Object.create(null);

console.log(pureMap.__proto__); // => undefined (親がいない!)
console.log(Object.prototype.hasOwnProperty.call(pureMap, ‘toString’)); // => false

親がいないため、どれだけ悪質なキーを代入しようとしても、`Object.prototype` を汚染することは物理不可能です。これはNode.jsなどでリクエストパラメータを扱う際に非常に有効なテクニックです。

鉄則3: 信頼できないデータはライブラリや安全な手法で処理する

モダンなアプリケーション開発では、オブジェクトのマージを自前で実装せず、セキュリティが担保された定評のあるライブラリ(Lodashの `_.merge` の最新版など)を使うか、あるいは構造化クローンや適切なバリデーション(ZodやJoiなど)を挟んで、予期せぬキーをあらかじめ排除するのがベストプラクティスです。

—

まとめ

いかがでしたでしょうか?
今回は、JavaScriptのプロトタイプチェーンという美しい言語仕様の裏に潜む「プロトタイプ汚染」のメカニズムと、その防御手法について解説しました。

  • JavaScriptのすべてのオブジェクトは `Object.prototype` を頂点とする血縁関係にある。
  • 外部からの入力を無防備にオブジェクトのキーとして代入すると、大元のプロトタイプが書き換わり、アプリ全体が汚染される。
  • `__proto__` などの危険なキーをブロックするか、そもそも親を持たない `Object.create(null)` を活用して身を守る。

このポイントさえ押さえておけば、セキュリティインシデントの大きな芽を一つ摘むことができます。
ここをクリアしたあなたなら、より堅牢で美しいコードを書けるはずです。ぜひ明日の開発から意識してみてくださいね!

タイトルとURLをコピーしました