零弐壱蜂

JavaScriptを利用したブラウザのプライベートモード判定についての考察

概要

ブラウザのプライベートモード(またはシークレットモード、InPrivateブラウジングなど)の識別は、JavaScriptでは困難である。

結論として、執筆時点ではJavaScriptを利用して全ブラウザ・バージョンにわたりプライベートモードを100%判定する方法は存在しない。ある程度は判定可能だが、それも限定条件下でしか作用しない。

背景

判定が不可能な主な要因は、ブラウザベンダーがプライベートモードを示す標準APIを提供していない点にある1
プライベートモードはプライバシー保護が目的であり、容易な検知はプライバシー保護の目的を損ない、フィンガープリンティングに悪用されるリスクもあるため、意図的に制限されている。

ハックの限界

標準APIがないため、開発者はハックを利用した手法(非公式な回避策や挙動の利用)で検出を試みている。

挙動の差異を利用するハック

プライベートモードで制限されるAPIの挙動差異を利用して検出を試みる。

主なAPI

  • ストレージAPI(localStorage, sessionStorage, IndexedDB)
    • iOS Safari: localStorageブロック、QuotaExceededError発生(iOS 10以前)
      • プライベートモードではメモリ上のみでデータを管理し、永続化を避けるため
    • Chrome: セッション終了時にデータ削除
      • データは一時的に保存されるが、ブラウザを閉じるとすべて削除
    • Firefox: indexedDB.open()失敗(旧バージョン)
      • プライベートモードではIndexedDB接続自体を拒否する実装があった
  • ディスククォータ(navigator.storage.estimate())
    • Chrome: 120MB程度の制限(通常モードはGB単位)
      • プライベートモードでは最小限のストレージのみ割り当て
    • 他ブラウザ: 0バイトや極端に小さい値
      • ディスク永続化を基本的に拒否する設計思想

ただし、これらは副作用を利用した判定であり、ディスク逼迫で同様のエラーが発生する場合もある。そのため、複数APIの挙動を組み合わせて「プライベートモードである蓋然性が高い」と推測するのが基本的なアプローチとなる。

サンプルコード

 warning

以下はサンプルコードであり、ブラウザ間の差異やエラー処理などは未考慮のため、実運用には適さない。

/**
 * プライベートモード検出を試みる
 * 複数のAPIチェックを組み合わせて蓋然性を判定
 */
async function detectPrivateMode() {
  let score = 0;
  const maxChecks = 2; // 現在のチェック数

  // 1. localStorage チェック
  // プライベートモードではQuotaExceededErrorが発生する可能性
  try {
    const testKey = '__private_mode_test__';
    localStorage.setItem(testKey, '1');
    localStorage.removeItem(testKey);
  } catch (e) {
    // iOS 10以前のSafariプライベートモードではここで失敗
    score++;
  }

  // 2. IndexedDB チェック
  // プライベートモードでは接続自体が失敗する場合がある
  const dbResult = await checkIndexedDB();
  if (!dbResult) score++;

  // 3. ディスククォータチェックを追加可能
  // navigator.storage.estimate()で容量を確認

  return {
    isPrivate: score > 0,
    confidence: score / maxChecks, // 0〜1の範囲で信頼度を表現
    detectedChecks: score, // 検出されたチェック数
  };
}

/**
 * IndexedDBの利用可否を確認
 * Firefox旧バージョンではプライベートモードで失敗
 */
async function checkIndexedDB() {
  return new Promise((resolve) => {
    try {
      const dbName = '__private_mode_test__';
      const request = indexedDB.open(dbName);

      // エラーが発生 = プライベートモードの可能性
      request.onerror = () => resolve(false);

      // 成功 = 通常モードの可能性が高い
      request.onsuccess = (e) => {
        const db = e.target.result;
        db.close();
        // テストDBを削除してクリーンアップ
        indexedDB.deleteDatabase(dbName);
        resolve(true);
      };

      // 1秒でタイムアウト(無限待機防止)
      setTimeout(() => resolve(true), 1000);
    } catch {
      // 例外発生 = プライベートモードの可能性
      resolve(false);
    }
  });
}

// 使用例
detectPrivateMode().then((result) => {
  if (result.isPrivate) {
    // プライベートモードの可能性が高い
    console.log(`検出結果: プライベートモードの可能性`);
    console.log(`信頼度: ${result.confidence * 100}%`);
    console.log(`検出チェック数: ${result.detectedChecks}`);

    // アプリケーションの動作を調整
    // 例:代替ストレージを使用、機能制限など
  }
});

実際のユースケース

プライベートモード検出が必要とされる場面は存在するが、完全な検出が困難であることを理解した上での利用が重要である。

検出が要求される場面

  • ストレージ依存のアプリケーション

    • localStorageにデータ保存が必須のサービス
    • プライベートモードでは機能制限の警告表示
  • 分析・トラッキング

    • ユーザー行動分析の精度向上
    • プライベートモードユーザーの把握
  • 課金システム

    • 有料コンテンツのペイウォール
    • トライアル制限の実装

推奨される対応方法

検出結果に大きく依存するのではなく、段階的な機能制限(グレースフル・フォールバック)を実施する。

ブラウザ別実装差異

プライベートモードの実装はブラウザごとに大きく異なる。

ブラウザlocalStorageIndexedDBディスククォータ特記事項
Chrome/Edge一時保存、セッション終了時削除利用可能120MB程度検出が比較的困難
Firefox利用可能旧バージョンでブロック極小値バージョンで挙動変化
Safari(iOS)QuotaExceededError(iOS 10以前)利用可能0バイトiOS 10以前は検出容易
Safari(macOS)利用可能利用可能制限ありiOSと挙動が異なる
OperaChromeと同様Chromeと同様Chromeと同様Chromiumベース

ハック的手法の限界

1. ブラウザアップデートによる陳腐化

ハック的手法が依存するAPIの挙動(副作用)は、Web標準として保証されたものではなく、特定バージョンのブラウザにおける実装の詳細に過ぎない。

ブラウザがセキュリティパッチ、新機能追加、仕様変更などでアップデートされるたびに、これらの副作用は変更される可能性がある。例えば、以前はエラーを返していたストレージ操作がエラーを返さなくなったり、逆の変更が生じたりする可能性がある。

さらに、ブラウザベンダーがプライバシー保護強化の一環として、検出手法を意図的に無効化する(プライベートモードでも通常モードと同じ挙動に見せる)ケースもある。

2. プラットフォーム間の実装差

プライベートモードの実装方法は、ブラウザ(Chrome、Firefox、Safari、Edge等)や動作するOS(Windows、macOS、Android、iOS等)によって差異が存在する。

例えば、iOS上のSafariにおけるプライベートモードのストレージ制限は、デスクトップ版Chromeのシークレットモードとは異なる挙動を示す。このような環境による仕様差があるため、一貫して動作する検出ロジックの作成は困難である。

3. 誤検知のリスク

偽陽性(False Positive): 実際は通常モードだが、プライベートモードと誤判定。例:ディスク容量不足でストレージAPIがエラーを返す、ブラウザ拡張機能がAPIに干渉。

偽陰性(False Negative): 実際はプライベートモードだが、検出に失敗。例:ブラウザ実装の変更により、プライベートモード固有の挙動が隠蔽される。

まとめ

JavaScriptを用いたフロントエンドでのプライベートモード判定は、ハック的手法の技術的限界により、確実な実現は困難であり、実運用上のリスクが高い。

注釈

  1. W3CやWHATWGのHTML仕様にはプライベートモード検出用のAPIは定義されていない。MDN Web Docsでも公式な検出方法は文書化されていない。