はじめに
― Recursive Merge と Clone 機能が招く深刻な脆弱性 ―
現代のJavaScriptアプリケーションでは、
設定マージ、オブジェクトの複製、ユーザーデータの更新など、
「オブジェクトをマージする処理」が数えきれないほど登場します。
…が、ここにこそ悪魔が潜んでいます。
Prototype Pollution(プロトタイプ汚染)は、
“アプリ内のすべてのオブジェクト” に影響を与えうる 非常に危険な脆弱性です。
今回は、実際のクローンアルバム機能を例に、攻撃方法から影響範囲、そして対策までを徹底的に解説します。
1. Property Injectionとは何か
Property Injectionとは、アプリが持つ
- recursive merge(再帰的マージ)
- deep clone(深い複製)
といった便利機能を悪用して、
攻撃者が任意のプロパティをサーバ側オブジェクトに注入する攻撃手法です。
入力キーを正しくフィルタしていない場合、
攻撃者は __proto__ や constructor などの 特殊キー を注入し、
アプリケーション全体のプロトタイプチェーンを書き換えることができます。
2. 脆弱な recursiveMerge の例
まずは典型的な「設定マージ関数」から。
function recursiveMerge(target, source) {
for (let key in source) {
if (source[key] instanceof Object) {
if (!target[key]) target[key] = {};
recursiveMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
攻撃者は次のリクエストを送るだけで Prototype Pollution を起こせます:
{
"__proto__": { "newProperty": "value" }
}
なぜ?
target["__proto__"] = {...} は結果的にこうなります:
Object.prototype.newProperty = "value";
全オブジェクトに newProperty が生える。チーン。
3. 実例:Clone Album 機能の破壊的コンボ
攻撃の舞台となるのは、次のような「アルバム複製機能」。
クライアント側(ユーザーが入力する部分)
<form action="/clone-album/1" method="post">
<input type="text" name="newAlbumName" placeholder="Enter new album name">
<button type="submit">Clone Album</button>
</form>
見た目はただのテキスト入力。しかしサーバ側は…
サーバ側の処理(核心)
const payload = JSON.parse(newAlbumName);
merge(clonedAlbum, payload);
攻撃者は「アルバム名」に JSON を入れられる状態。
さらに merge() も完全に無防備:
function merge(to, from) {
for (let key in from) {
if (typeof to[key] == "object" && typeof from[key] == "object") {
merge(to[key], from[key]);
} else {
to[key] = from[key];
}
}
}
つまり…
攻撃者はアルバム名に以下を入力できる:
{"__proto__": { "newProperty": "hacked" }}
サーバ側はそれを JSON として受理、merge、そして…
clonedAlbum.__proto__.newProperty = "hacked"
結果:
friend オブジェクト全員が newProperty を持つようになる。
4. どうして全オブジェクトに影響するのか?
JavaScript は「プロトタイプチェーン」という仕組みでプロパティを探索します。
- まず自分自身を見る
- 無ければ
__proto__を見る - さらに上のプロトタイプへ…
つまり prototype を汚染すると:
- 既存のすべての friend オブジェクト
- 未来に作られる friend オブジェクト
が等しく影響を受ける。
5. なぜ画面にも newProperty が出てしまうのか?
テンプレート側のコードがこちら:
<% for (let key in friend) { %>
<p><%= key %>: <%= friend[key] %></p>
<% } %>
for...in は:
- 自身のプロパティだけでなく
- プロトタイプの列挙可能プロパティも拾う
結果:
→ newProperty: hacked が画面に堂々と登場する。
6. Property Injection が危険な理由
| 危険ポイント | 内容 |
|---|---|
| キー無検証 |
__proto__ をそのまま処理してしまう |
| プロトタイプ汚染 | 全オブジェクトが汚染される |
| EJSで表示 |
for...in がprototype由来プロパティを表示 |
| 他攻撃との連携 | XSS と組み合わさるとアプリが崩壊 |
「Prototype Pollution は単体だと地味」
と言われることがありますが、
GUIに表示され、ロジックにも影響し、他の脆弱性と連鎖する
となれば話は別。破壊力は一気に跳ね上がります。
7. 防御方法
① 危険キーをブロックする
const blocked = ["__proto__", "constructor", "prototype"];
if (blocked.includes(key)) continue;
② マージ・クローン処理は自前実装しない
lodashでも古いバージョンは脆弱だったため常に最新版を使用すること。
③ 文字列入力を JSON.parse しない
アルバム名は文字列で十分。
仕様を変えられるならここが最大の防御ポイント。
④ テンプレートで hasOwnProperty を利用
if (!Object.prototype.hasOwnProperty.call(friend, key)) continue;
おわりに
Prototype Pollution は地味に見えて、
実際は「アプリケーションの土台(プロトタイプ)に穴を開ける攻撃」。
小さなマージ関数ひとつが、
大規模なセキュリティ事故の始まりになることもあります。
コードレビューで見逃されやすい分、
「知っているかどうか」だけで攻撃と防御の差が大きく出る分野です。