プロトタイプ汚染:JavaScriptの「見えない侵入経路」を塞ぐ技術
現場でコードをレビューしていると、JSONオブジェクトを再帰的にマージする便利な関数を自作しているエンジニアによく出会う。一見、効率的で美しいコードに見えるが、実はそこに「JavaScriptの深淵」が潜んでいる。今日は、モダンWebアプリの盲点となる「プロトタイプ汚染(Prototype Pollution)」について、現場の知見を交えて徹底的に解剖しよう。
1. プロトタイプ汚染とは何か?
JavaScriptはプロトタイプベースの言語だ。すべてのオブジェクトは __proto__ というプロパティを持ち、親となるオブジェクト(Object.prototype)の機能を受け継いでいる。
攻撃者は、この仕組みを悪用する。もし、あなたが書いたマージ関数が、ユーザーからの入力を深く考えずに Object.prototype に書き込んでしまったらどうなるか? アプリケーション全体で使われる「標準オブジェクト」の動作が、攻撃者の都合のいいように書き換えられてしまうのだ。これは単なるバグではなく、アプリケーション全体の挙動を乗っ取る「論理的エクスプロイト」である。
2. 現場でよく見る「危ないマージ関数」
まずは、なぜ攻撃が成立するのか、その脆弱な実装例を見てほしい。
// 脆弱なマージ関数の例
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
// ここで再帰的にマージを行う際、__proto__ がチェックされていない!
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
この関数に対し、以下のような不正なJSONを投げつけるとどうなるか。
{
"__proto__": {
"isAdmin": true
}
}
このリクエストを送った後、Webアプリ上のあらゆるオブジェクトに対して obj.isAdmin を参照すると、すべて true が返ってくるようになる。認証ロジックが user.isAdmin を参照している場合、認証バイパスが完成する。これがプロトタイプ汚染の破壊力だ。
3. 実践:防御の鉄則
では、この脆弱性をどう塞ぐか。アプローチは3つある。
A. キーのフィルタリング(根本解決)
マージ処理を行う際、__proto__、constructor、prototype といった「危険なキー」を明示的に弾くこと。これが最も確実だ。
function secureMerge(target, source) {
const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];
for (let key in source) {
// 危険なキーが含まれていたら即座にスキップ
if (forbiddenKeys.includes(key)) continue;
if (typeof source[key] === 'object' && source[key] !== null) {
secureMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
}
B. Object.freeze による強制封印
環境自体を汚染から守るために、起動時の初期化コードで Object.prototype を凍結する方法がある。
// アプリケーションのエントリーポイント(index.js等)で実行
Object.freeze(Object.prototype);
// これ以降、Object.prototypeへの書き込みは無視される(またはエラーになる)
これを行えば、万が一マージ関数にバグが残っていたとしても、コアとなるオブジェクトへの汚染は防げる。ただし、外部ライブラリが Object.prototype を拡張している場合、動作に支障が出る可能性があるため、導入には検証が必要だ。
C. JSON.parse の検証(WAF/Middleware)
Node.js環境であれば、express などのミドルウェアでリクエストボディを解析する前に、__proto__ などのキーが含まれていないかチェックするバリデーションを挟むのが最も安全だ。
// Expressでの簡易的なガード
app.use((req, res, next) => {
const bodyString = JSON.stringify(req.body);
if (bodyString.includes('"__proto__"') || bodyString.includes('"constructor"')) {
return res.status(400).send('Invalid request: Prototype pollution attempt detected.');
}
next();
});
4. セキュリティチーフからの助言
プロトタイプ汚染は、SQLインジェクションのように「一撃でDBが抜かれる」ような派手さはないかもしれない。しかし、その分、アプリケーション内部で静かに汚染が広がり、気づいたときにはシステム全体が乗っ取られているという最悪のシナリオを招く。
以下のチェックリストを日々の開発プロセスに組み込んでほしい。
1. ライブラリの更新を怠らない: lodash や jQuery の古いバージョンには、この脆弱性が山ほどある。npm audit を定期的に実行せよ。
2. JSON入力を信頼しない: APIの入力値はすべて「悪意がある」と見なすのが、セキュリティの鉄則だ。
3. Mapオブジェクトの検討: 頻繁にキー・バリューの動的な追加が必要なら、Object ではなく new Map() を使うことを強く推奨する。Map はプロトタイプ汚染の影響を直接受けない。
技術は常に進化するが、攻撃者の視点はいつの時代も「プログラムの前提条件の隙」を突く。堅牢なシステムとは、この「前提の隙」を一つずつ、泥臭く埋めていった先にあるものだ。君たちのコードが、明日も安全であることを願っている。
コメント