零弐壱蜂

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

11 min read

概要

ブラウザのプライベートモード(またはシークレットモード、InPrivateブラウジングなど)を識別したい。しかしながら、JavaScriptによる確実な識別は困難である。

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

背景

判定が不可能な主な要因は、ブラウザベンダーがプライベートモードを示す標準APIが提供されていない点にある。
これはプライベートモードが「追跡されたくない」といったユーザーのプライバシー保護を重視するためと考えられる。容易な検知はプライバシー保護の目的を損ないフィンガープリント(fingerprint)に悪用されるリスクもあるためか、明確な判定手段は提供されていない。

ハックの限界

標準APIがないため、いわゆるハックを利用した手法(非公式な回避策や挙動の利用)による検出がプライベートモードを判定するために試みられている。

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

これらの手法は、プライベートモードと通常モードで特定のWeb APIの挙動の差異に基づいたものである。

プライベートモードは、プライバシー保護の観点からデータ永続化を避けるため一部API機能を制限することがあるらしい。この「制限」や「通常と異なる挙動」を検知し、プライベートモードの可能性を推測する。

主に以下のAPI群の挙動差異を判定に利用する。

  • ストレージAPI (localStorage, sessionStorage, IndexedDB):
    • プライベートモードでは、ディスクへの永続的な書き込みを避けるため挙動が異なる。
      • localStorage/sessionStorage: データ書き込み (setItem) 時に、十分なストレージ容量があってもQuotaExceededError等のエラーが意図的に発生させられることがある。
      • IndexedDB: 永続的なデータベース接続の確立が許可されず、indexedDB.open()リクエストが失敗し、onerrorイベントハンドラが呼び出されるケースもある。
    • プライベートセッション中はストレージが一時的(メモリ上のみなど)に扱われる、あるいはアクセス自体が厳しく制限される実装のためと考えられる。
  • ディスククォータ (navigator.storage.estimate()):
    • ブラウザは通常、ウェブサイトが利用できるディスク容量(クォータ)を管理しており、navigator.storage.estimate()メソッドで推定値を取得できる。
    • プライベートモードでは、セッション終了時にデータを破棄するため、永続的なディスク領域をほとんど、あるいは全く割り当てないことがあるらしい。
      • 結果として、このAPIで取得される利用可能ディスク容量の推定値 (quota プロパティ) が、通常モードと比較して極端に小さい値(例えば0バイト)として確認できる場合があり、これがプライベートモードの指標とされる。
  • その他のAPI:
    • 過去、ローカルファイルシステムへのアクセスを試みるFileSystem API(現在は非推奨)などが、プライベートモードでの利用制限を根拠に検出手段として利用されたことがある。
      • しかし、これらのAPIは標準化の停滞や目的外利用による挙動変更などにより、現在では安定した判定手段としては利用できない。

これらの挙動差は副作用を利用した判定に過ぎない。例えばlocalStorageのエラーは、ディスク逼迫でも起こり得るため、複数APIの挙動を組み合わせ、「プライベートモードである蓋然性が高い」と推測するのがハックを利用した際の基本アプローチになる。

ハック的手法のコード例

 warning

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

/** プライベートモードの可能性を推測する */
async function guessIfPrivateMode() {
  let privateIndicators = 0; // 指標カウンター
  const checks = { localStorage: 'unknown', indexedDB: 'unknown' }; // チェック結果

  // 1. localStorage アクセス試行
  try {
    localStorage.setItem('__test_private_mode__', '1');
    localStorage.removeItem('__test_private_mode__');
    checks.localStorage = 'writable';
  } catch (e) {
    privateIndicators++;
    checks.localStorage = 'error';
    console.warn('localStorage access failed:', e);
  }

  // 2. IndexedDB アクセス試行 (非同期)
  await new Promise((resolve) => {
    try {
      const request = indexedDB.open('__test_private_mode__');
      request.onerror = (event) => {
        privateIndicators++;
        checks.indexedDB = 'error';
        console.warn('IndexedDB open failed:', event.target.error);
        resolve();
      };
      request.onsuccess = (event) => {
        try {
          event.target.result.close();
          indexedDB.deleteDatabase('__test_private_mode__');
        } catch (closeErr) {
          /* ignore */
        }
        checks.indexedDB = 'success';
        resolve();
      };
      setTimeout(() => {
        // Timeout
        if (checks.indexedDB === 'unknown') {
          checks.indexedDB = 'timeout';
          resolve();
        }
      }, 1000);
    } catch (e) {
      privateIndicators++;
      checks.indexedDB = 'exception';
      console.warn('IndexedDB access threw exception:', e);
      resolve();
    }
  });

  // 3. 他のチェックも追加可能...

  // 結果判定
  const isPotentiallyPrivate = privateIndicators > 0;
  const confidence = privateIndicators / Object.keys(checks).length;

  console.log('Detection checks:', checks);
  console.log(`Private indicators: ${privateIndicators}`);

  return {
    isPotentiallyPrivate,
    confidence: parseFloat(confidence.toFixed(2)),
    checks,
  };
}

// 実行例
guessIfPrivateMode().then((result) => {
  console.log(`Is potentially private? : ${result.isPotentiallyPrivate}`);
  console.log(`Confidence score (simple): ${result.confidence}`);
});

ハックの限界

このようなハックの実用性には限界がある。

1. ブラウザアップデートによる検出ロジックの陳腐化

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

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

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

結果として、一度確立したかに見えた検出ロジックも、ブラウザのアップデートによって有効性を失うリスクが高い。

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

プライベートモードの実装方法は、ブラウザ(Chrome, Firefox, Safari, Edge等)や動作するOS(Windows, macOS, Android, iOS等)によって差異が存在する。例えば、iOS上のSafariにおけるプライベートモードのストレージ制限は、デスクトップ版Chromeのシークレットモードとは異なる挙動である。

環境による仕様差があるため、一貫して動作する検出ロジックを作成することは困難である。

3. 判定は不正確:誤検知(偽陽性・偽陰性)のリスク

これらのハック的手法は、プライベートモードであることを示す確実な証拠ではなく、あくまで状況証拠に基づいているため、本質的に判定精度には限界があり、誤検知のリスクがある。

具体的なリスクのひとつが偽陽性(False Positive)である。これは、実際には通常モードであるにも関わらず、プライベートモードであると誤って判定してしまうケースを指す。例えば、ユーザーのディスク容量が本当に不足していてストレージAPIがエラーを返した場合や、特定のブラウザ拡張機能がストレージAPIの挙動に干渉した場合などに発生し得る。

もうひとつのリスクが偽陰性(False Negative)である。これは、実際にはプライベートモードであるにも関わらず、通常モードであると誤判定(検出に失敗)してしまうケースを指す。例えば、ブラウザの実装変更により、プライベートモード固有の挙動(副作用)が隠蔽され、検出できなくなった場合などに起こり得る。

これらの誤検知は、アプリケーションがユーザーのモードに基づいて不適切な動作(例:プライベートモードのユーザーに不要な警告を表示する、通常モードのユーザーにプライベートモード限定機能を提供してしまう)を引き起こす原因となる。

まとめ

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