Prototype Pollution: JavaScriptの脆弱性がアプリ全体を汚染 ☣️

Webアプリケーションのセキュリティの世界では、いくつかの脆弱性は劇的なクラッシュや明白な侵害とともに自己主張します。その他は静かに動作し、アプリケーションの根幹から破壊します。Prototype pollutionは後者のカテゴリーに属し、JavaScriptのプロトタイプ継承を悪用して、単一の悪意のある入力でアプリ内のすべてのオブジェクトを汚染する微妙ながらも壊滅的な脆弱性です。
JavaScriptのPrototype Chainの理解
攻撃に入る前に、JavaScriptのプロトタイプベースの継承がどのように機能するかを理解する必要があります。従来のオブジェクト指向言語とは異なり、JavaScriptはプロトタイプを使ってオブジェクト間でプロパティやメソッドを共有します。すべてのJavaScriptオブジェクトは、内部リンクを持ち、それがそのプロトタイプと呼ばれる別のオブジェクトに接続されており、これがプロトタイプチェーンを形成します。
オブジェクトのプロパティにアクセスするとき、JavaScriptはまずそのプロパティが直接オブジェクトに存在するかを確認します。存在しなければ、プロトタイプチェーンを上っていき、Object.prototypeに到達するまで各プロトタイプを確認します。この仕組みは効率的なプロパティ共有を可能にしますが、同時に危険な攻撃面も作り出します。
次のような一見無害なコードを考えてみてください:
const user = { name: 'Alice' };
console.log(user.toString); // [Function: toString]
userオブジェクトにtoStringプロパティを定義していなくても、JavaScriptはObject.prototypeまで遡ってtoStringを見つけ出します。この挙動はJavaScriptの基本的な動作ですが、攻撃者がこれらのプロトタイプを操作できると危険です。
Prototype Pollutionとは何か?
Prototype pollutionは、攻撃者が既存のJavaScriptの言語構造のプロトタイプにプロパティを注入できる脆弱性です。JavaScriptは__proto__やconstructor、prototypeなどの特殊なプロパティの修正を許可している点を悪用します。
攻撃は、アプリケーションがユーザが制御可能なデータを適切に検証せずに既存のオブジェクトにマージする際に発生します。攻撃者は悪意のある入力を作成し、Object.prototypeを変更します。JavaScriptのほぼすべてのオブジェクトはObject.prototypeから継承しているため、この一つの変更がアプリ全体に影響します。
これらの脆弱性は、JSONデータを信頼できないソースから処理する際に、キーのサニタイズを行わずに再帰的にオブジェクトをマージする場合に特に発生しやすいです。
実際の攻撃例:プロトタイプ汚染の仕組み
実際の攻撃例を、人気ライブラリに見られる脆弱なマージ関数を使って解説します。以下は簡略化した例です:
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// 通常の使用例
const user = {};
merge(user, { name: 'Alice', role: 'user' });
console.log(user); // { name: 'Alice', role: 'user' }
この関数は無害に見えます。ソースオブジェクトのプロパティを再帰的にターゲットにマージします。しかし、攻撃者が悪意のある入力を提供した場合に何が起こるか見てみましょう:
// 悪意のあるペイロード
const maliciousInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
// プロトタイプを汚染
merge({}, maliciousInput);
// これで全てのオブジェクトにisAdminプロパティが継承される
const normalUser = {};
console.log(normalUser.isAdmin); // true - 汚染成功!
攻撃者は__proto__の特殊なプロパティを悪用してObject.prototypeにアクセスし、isAdminプロパティを注入しました。これにより、アプリ内のすべてのオブジェクト(新規作成されたものも含む)がこの汚染されたプロパティを継承します。
実世界の影響:Lodashのケーススタディ
人気のLodashライブラリも、defaultsDeepやmerge、mergeWithといった関数において、__proto__を通じてObjectのプロトタイプを変更できる脆弱性に影響を受けています。これにより、世界中の何百万ものアプリケーションに影響を及ぼす可能性があります。
以下はLodashのmerge関数を使った脆弱なコード例です:
const _ = require('lodash');
const express = require('express');
const app = express();
app.use(express.json());
app.post('/api/update-settings', (req, res) => {
const userSettings = {};
// 脆弱:信頼できないユーザ入力をマージ
_.merge(userSettings, req.body);
// アプリ内の後続処理
if (userSettings.isAdmin) {
// 管理者権限を付与
return res.json({ access: 'admin' });
}
res.json({ access: 'user' });
});
攻撃者は次のペイロードを送信できます:
{
"__proto__": {
"isAdmin": true
}
}
この一つのリクエストでプロトタイプが汚染され、アプリ内のすべてのオブジェクト(認証チェックに使われるものも含む)がisAdminがtrueに設定された状態を継承します。影響はアプリ全体に波及します。
汚染からリモートコード実行へ
Prototype pollutionの結果は、単なる権限昇格を超え、Node.js環境では、巧妙な攻撃者が他の脆弱性と組み合わせてリモートコード実行(RCE)を達成することも可能です。これは「ガジェットチェーン」と呼ばれる技術を通じて行われ、汚染されたプロパティが既存のコードパスと予期せぬ形で相互作用します。
例えば、アプリが子プロセスの生成を行い、継承されたプロパティをコマンド構築に利用している場合、攻撃者はプロトタイプ汚染を通じて悪意のあるコマンドを注入できる可能性があります:
// 脆弱なコード
const { spawn } = require('child_process');
function executeCommand(options) {
const defaultOptions = {};
// オプションは汚染されたプロパティを継承している可能性
const finalOptions = Object.assign(defaultOptions, options);
spawn('node', [finalOptions.script || 'default.js'], {
shell: finalOptions.shell || false,
env: finalOptions.env || process.env
});
}
攻撃者が{"shell": true, "script": "; malicious-command"}をプロトタイプに汚染させると、この関数が十分なプロパティチェックなしに実行された場合、コード実行が可能になります。
プロトタイプ汚染によるクロスサイトスクリプティング
攻撃者はinnerHTMLやsrc、onerrorといったプロパティを汚染し、その後アプリがこれらのプロパティを参照してDOMに配置すると、クロスサイトスクリプティング(XSS)が可能になります。このバリアントはクライアントサイドのJavaScriptフレームワークで特に危険です:
// 脆弱なテンプレートレンダリング
function renderContent(element, data) {
element.innerHTML = data.content || 'Default content';
}
// 攻撃者がプロトタイプを汚染
const malicious = JSON.parse('{"__proto__": {"content": "<img src=x onerror=alert(document.cookie)>"}}');
merge({}, malicious);
// 後のコード
const emptyData = {};
renderContent(document.getElementById('output'), emptyData);
// 汚染されたプロトタイプを通じてXSSが発動!
実環境での検出
高度なファジング技術を用いた研究者たちは、従来の方法では検出できなかったゼロデイのプロトタイプ汚染脆弱性を65件発見しています。これは、この問題がいかに広範囲にわたっているかを示しています。プロトタイプ汚染の難しさは、その微妙さにあります。SQLインジェクションやXSSのように即座にエラーや明白な効果をもたらすわけではなく、静かにコードベースに潜み、適切な条件下で壊滅的な失敗を引き起こします。
防御的コーディング:汚染耐性のあるアプリケーション構築
プロトタイプ汚染からアプリケーションを守るには、多層的なアプローチが必要です。安全なコーディング慣行、入力検証、アーキテクチャの決定を組み合わせて行います。
Object.create(null)を辞書に使用
プロトタイプを持たないオブジェクトをObject.create(null)で作成すると、プロトタイプチェーンを断ち切り、汚染を防止できます。これは最も効果的な防御策の一つです:
// 安全:プロトタイプチェーンなし
const userSettings = Object.create(null);
userSettings.name = 'Alice';
// 汚染されない
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign(userSettings, malicious);
console.log(userSettings.isAdmin); // undefined - 保護済み!
この方法で作成されたオブジェクトはプロトタイプを持たず、プロトタイプ汚染攻撃に対して免疫があります。ユーザ制御データを保持するオブジェクトにはこのパターンを使用してください。
プロパティの検証とサニタイズ
ユーザ入力を処理する前に、オブジェクトのキーを常に検証・サニタイズします:
function secureMerge(target, source) {
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
for (let key in source) {
// 危険なキーをブロック
if (dangerousKeys.includes(key)) {
continue;
}
// 追加の検証
if (typeof key !== 'string' || key.startsWith('_')) {
continue;
}
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = Object.create(null);
secureMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
重要なオブジェクトにはObject.freeze()を使用
プロトタイプや重要なオブジェクトを凍結して変更を防ぎます:
// プロトタイプ汚染を防止
Object.freeze(Object.prototype);
Object.freeze(Object);
// 設定オブジェクトを凍結
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000
});
この方法は効果的ですが、プロトタイプの変更に依存する正当なコードを壊す可能性もあるため、セキュリティ上重要なオブジェクトに限定して使用してください。
Mapをプレーンオブジェクトの代わりに使用
キーと値のペアを格納するには、Mapを使用するのがベストプラクティスです。MapはObject.prototypeを継承しません:
// 安全なオブジェクトの代替
const userSettings = new Map();
userSettings.set('name', 'Alice');
userSettings.set('role', 'user');
// プロトタイプ汚染の影響を受けない
console.log(userSettings.get('isAdmin')); // undefined
Mapはキーと値の管理にクリーンなAPIを提供し、プロトタイプ汚染のリスクを完全に排除します。
スキーマ検証の導入
スキーマ検証ライブラリを使って、厳格なオブジェクト構造を強制します:
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
role: Joi.string().valid('user', 'admin').required()
}).unknown(false); // 未知のプロパティを拒否
app.post('/api/users', (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details });
}
// 検証済みデータの安全な使用
processUser(value);
});
スキーマ検証は、予期しないプロパティがアプリケーションロジックに到達するのを防ぎ、プロトタイプ汚染の試みを入口でブロックします。
依存関係の定期的な更新
依存関係を最新に保つことも重要です。特にセキュリティに関わるライブラリは、最新バージョンで脆弱性が修正されている場合があります。npm auditやSnykなどのツールを使って脆弱性を特定しましょう:
npm audit
npm audit fix
厳格モードの有効化
JavaScriptのstrict modeは追加の保護を提供します:
'use strict';
// 厳格モードは誤ってグローバル変数を作成するのを防ぎ、静かなエラーを例外にします
function processData(input) {
// より安全な実行環境
}
Prototype Pollutionの検出方法
セキュリティテストにおいて、プロトタイプ汚染テストを含めることが推奨されます:
describe('Prototype Pollution Tests', () => {
it('should not pollute Object.prototype', () => {
const original = Object.prototype.toString;
// 汚染を試行
const malicious = { "__proto__": { "isAdmin": true } };
merge({}, malicious);
// 汚染が起きていないか確認
const testObj = {};
expect(testObj.isAdmin).toBeUndefined();
expect(Object.prototype.toString).toBe(original);
});
it('should reject __proto__ in JSON input', () => {
const input = '{"__proto__": {"polluted": true}}';
const result = safeJSONParse(input);
expect({}.polluted).toBeUndefined();
});
});
より安全な設計のために
Prototype pollutionは、アプリケーションセキュリティのより広い原則の一例です:ディフェンス・イン・デプス(多層防御)。単一の技術だけではリスクを完全に排除できません。複数の防御策を重ねることが重要です:
- ユーザーデータには
Object.create(null)を使用 - すべての入力を検証・サニタイズ
- 重要なオブジェクトやプロトタイプを凍結
- 辞書にはMapを使用
- 厳格なスキーマ検証を実施
- 依存関係を最新に保つ
- 定期的なセキュリティテスト
- 本番環境で異常なプロパティアクセスを監視
まとめ
Prototype pollutionは、一見無害に見える言語機能が、信頼できない入力を扱う際に深刻なセキュリティ脆弱性に変わる例です。JavaScriptのプロトタイプチェーンを悪用して、攻撃者はアプリ内のすべてのオブジェクトを汚染し、権限昇格やサービス拒否、リモートコード実行を引き起こす可能性があります。
幸いなことに、プロトタイプ汚染は規律あるコーディング慣行によって防ぐことができます。攻撃ベクトルを理解し、堅牢な入力検証を実施し、安全なオブジェクト作成パターンを採用し、依存関係を最新に保つことで、この微妙ながらも壊滅的な脆弱性に抵抗できるJavaScriptアプリケーションを構築できます。
覚えておいてください:JavaScriptでは、アプリケーションの安全性はそのプロトタイプチェーンにかかっています。一つの汚染されたプロトタイプはアプリ全体を汚染しますが、適切な防御策を講じれば、オブジェクトを純粋に保ち、アプリケーションを安全に保つことが可能です。
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.