Security
9 min read
2755 views

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

IT
InstaTunnel Team
Published by our engineering team
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__constructorprototypeなどの特殊なプロパティの修正を許可している点を悪用します。

攻撃は、アプリケーションがユーザが制御可能なデータを適切に検証せずに既存のオブジェクトにマージする際に発生します。攻撃者は悪意のある入力を作成し、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ライブラリも、defaultsDeepmergemergeWithといった関数において、__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
  }
}

この一つのリクエストでプロトタイプが汚染され、アプリ内のすべてのオブジェクト(認証チェックに使われるものも含む)がisAdmintrueに設定された状態を継承します。影響はアプリ全体に波及します。

汚染からリモートコード実行へ

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"}をプロトタイプに汚染させると、この関数が十分なプロパティチェックなしに実行された場合、コード実行が可能になります。

プロトタイプ汚染によるクロスサイトスクリプティング

攻撃者はinnerHTMLsrconerrorといったプロパティを汚染し、その後アプリがこれらのプロパティを参照して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は、アプリケーションセキュリティのより広い原則の一例です:ディフェンス・イン・デプス(多層防御)。単一の技術だけではリスクを完全に排除できません。複数の防御策を重ねることが重要です:

  1. ユーザーデータにはObject.create(null)を使用
  2. すべての入力を検証・サニタイズ
  3. 重要なオブジェクトやプロトタイプを凍結
  4. 辞書にはMapを使用
  5. 厳格なスキーマ検証を実施
  6. 依存関係を最新に保つ
  7. 定期的なセキュリティテスト
  8. 本番環境で異常なプロパティアクセスを監視

まとめ

Prototype pollutionは、一見無害に見える言語機能が、信頼できない入力を扱う際に深刻なセキュリティ脆弱性に変わる例です。JavaScriptのプロトタイプチェーンを悪用して、攻撃者はアプリ内のすべてのオブジェクトを汚染し、権限昇格やサービス拒否、リモートコード実行を引き起こす可能性があります。

幸いなことに、プロトタイプ汚染は規律あるコーディング慣行によって防ぐことができます。攻撃ベクトルを理解し、堅牢な入力検証を実施し、安全なオブジェクト作成パターンを採用し、依存関係を最新に保つことで、この微妙ながらも壊滅的な脆弱性に抵抗できるJavaScriptアプリケーションを構築できます。

覚えておいてください:JavaScriptでは、アプリケーションの安全性はそのプロトタイプチェーンにかかっています。一つの汚染されたプロトタイプはアプリ全体を汚染しますが、適切な防御策を講じれば、オブジェクトを純粋に保ち、アプリケーションを安全に保つことが可能です。

Continue from this article into the most relevant product guides and workflows.

Related Topics

#prototype pollution, JavaScript security vulnerability, prototype chain attack, Object.prototype pollution, proto vulnerability, JavaScript exploit, web application security, Node.js security, Lodash vulnerability, prototype pollution prevention, Object.create null, JavaScript security best practices, prototype pollution attack, CVE JavaScript, secure coding JavaScript, JavaScript RCE, prototype pollution XSS, npm security, JavaScript input validation, Object.freeze security, prototype pollution defense, JavaScript malicious payload, dependency vulnerability, prototype-based inheritance, JavaScript security patterns, web security 2025, OWASP JavaScript, prototype pollution detection, secure merge function, JavaScript sanitization, Map vs Object security, prototype pollution testing, JavaScript security audit, client-side security, server-side JavaScript security, prototype chain exploitation, JavaScript remote code execution, privilege escalation JavaScript, gadget chain attack, JSON security, untrusted input handling, JavaScript prototype manipulation, secure object creation, JavaScript framework security, supply chain security, npm audit, Snyk security, prototype pollution mitigation, defensive programming JavaScript, zero-day JavaScript vulnerability, JavaScript security research, prototype pollution fuzzing, strict mode JavaScript, schema validation security, Joi validation, Express.js security, API security JavaScript, JavaScript security testing, secure development lifecycle, JavaScript threat model, prototype pollution impact, DOM-based XSS, JavaScript injection attack, secure JavaScript patterns, modern JavaScript security, TypeScript security, JavaScript security tools, penetration testing JavaScript, security code review, JavaScript vulnerability assessment, prototype pollution remediation, secure coding standards, JavaScript SAST, dynamic analysis JavaScript, JavaScript security checklist, OWASP Top 10 JavaScript, secure SDLC, JavaScript hardening, runtime protection JavaScript, JavaScript WAF bypass, security automation JavaScript, DevSecOps JavaScript, shift-left security, JavaScript security training, secure JavaScript development, prototype pollution case study, real-world JavaScript attacks, JavaScript security trends 2025, enterprise JavaScript security, microservices security, API gateway security, serverless security JavaScript, cloud security JavaScript, container security Node.js, Kubernetes security JavaScript, CI/CD security, GitHub security scanning, npm package security, open source security, software composition analysis, JavaScript security monitoring, application security testing, bug bounty JavaScript, responsible disclosure, CVE database, NVD JavaScript, security advisory, patch management JavaScript, version control security, code signing, JavaScript obfuscation security, minification security, webpack security, bundler security, frontend security, backend security, full-stack security JavaScript, React security, Vue security, Angular security, Next.js security, Express security, Fastify security, Koa security, JavaScript framework vulnerabilities

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles