API 間で一貫したユーザー アクティベーションを実現

Mustaq Ahmed
ジョー・メドレー
Joe Medley

悪意のあるスクリプトがポップアップや全画面などの機密性の高い API を悪用するのを防ぐため、ブラウザはユーザー アクティベーションを通じてこれらの API へのアクセスを制御します。ユーザー アクティベーションとは、ユーザー アクションに関連するブラウジング セッションの状態を指します。通常、「アクティブ」状態はユーザーが現在ページを操作しているか、ページの読み込み後にインタラクションを完了していることを意味します。「ユーザー操作」という言葉はよく使われていますが、同じアイデアに関して誤解を招くことがあります。たとえば、ユーザーがスワイプやフリックで操作してもページはアクティブになりません。そのため、スクリプトの観点からは、ユーザーの有効化にはなりません。

今日の主要なブラウザでは、ユーザー アクティベーションによる有効化制御型 API の制御に関して、動作が大きく異なります。Chrome での実装はトークンベースのモデルに基づいていましたが、すべての有効化制限のある API で一貫した動作を定義するには複雑すぎることが判明しました。たとえば、Chrome では postMessage() 呼び出しと setTimeout() 呼び出しによる有効化ゲート型 API への不完全なアクセスが可能でしたが、ユーザー アクティベーションは PromiseXHRゲームパッド操作などでサポートされていませんでした。これらのバグの中には、人気のあるが長年続いているバグもあります。

Chrome バージョン 72 では、ユーザー アクティベーション v2 がリリースされています。これにより、すべてのアクティベーション制限のある API でユーザー アクティベーションが利用可能になります。これにより、上記の不整合(および MessageChannels などのいくつかの不整合)が解決され、ユーザー アクティベーションに関するウェブ開発が容易になると Google は考えています。さらに新しい実装は、長期的にすべてのブラウザを統合するための提案された新しい仕様のリファレンス実装を提供します。

ユーザー アクティベーション v2 の仕組み

新しい API は、フレーム階層内の各 window オブジェクトで 2 ビットのユーザー アクティベーション状態を維持します。つまり、過去のユーザー アクティベーション状態(フレームがユーザー アクティベーションを検出した場合)のスティッキー ビットと、現在の状態(フレームが約 1 秒以内にユーザー アクティベーションを検出した場合)の一時的なビットです。スティッキー ビットは、設定後のフレームの存続期間中はリセットされません。一時的なビットはユーザー操作ごとに設定され、有効期限(約 1 秒)の後、またはアクティベーションを使用する API(window.open() など)の呼び出しによってリセットされます。

有効化ゲート型 API が異なれば、ユーザーの有効化に異なる形で依存します。新しい API では、これらの API 固有の動作は変更されません。たとえば、window.open() がユーザー アクティベーションを以前と同様に使用し、フレーム(またはそのサブフレーム)でユーザー アクションが確認された場合、Navigator.prototype.vibrate() が引き続き有効になるため、ユーザー アクティベーションごとに使用できるポップアップは 1 つのみです。

変更内容

  • User Activation v2 では、フレーム境界をまたぐユーザー アクティベーションの可視性の概念が形式化されています。ユーザーが特定のフレームを操作すると、そのオリジンに関係なく、含まれるすべてのフレーム(およびそれらのフレームのみ)がアクティブになります。(Chrome 72 では、すべての同一オリジン フレームに対する可視性を拡大するために、一時的な回避策があります。ユーザー アクティベーションをサブフレームに明示的に渡す方法が提供され次第、この回避策は削除されます)。
  • 有効化ゲート型 API が有効なフレームから呼び出されたが、イベント ハンドラコードの外部から呼び出された場合は、ユーザーの有効化状態が「アクティブ」である(期限切れでも消費されてもないなど)限り、API は動作します。ユーザー アクティベーション v2 より前は、無条件に失敗します。
  • 有効期限内の複数の未使用のユーザー インタラクションは、最後のインタラクションに対応する 1 つのアクティベーションに融合されます。

有効化ゲート型 API の整合性の例

以下に、window.open() を使用して開かれるポップアップ ウィンドウを 2 つの例を示します。これは、ユーザー アクティベーション v2 によって有効化ゲート型 API の動作が整合する仕組みを示しています。

チェーンされた setTimeout() 呼び出し

この例は setTimeout() デモのものです。click ハンドラが 1 秒以内にポップアップを開こうとすると、コードがどのように遅延を「生成」しても成功することが想定されます。ユーザー アクティベーション v2 はこの期待を満たすため、次の各イベント ハンドラは click にポップアップを開きます(100 ミリ秒の遅延)。

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

ユーザー アクティベーション v2 が有効になっていないと、テストしたすべてのブラウザで 2 番目のイベント ハンドラが失敗します。(最初のテストでも失敗する場合がある)。

クロスドメインの postMessage() 呼び出し

postMessage() デモの例を以下に示します。クロスオリジン サブフレームの click ハンドラが、2 つのメッセージを親フレームに直接送信するとします。親フレームは、次のメッセージのいずれかを受信したときにポップアップを開くことができる必要があります(両方は受信できません)。

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

ユーザー アクティベーション v2 がないと、親フレームは 2 番目のメッセージを受信したときにポップアップを開くことができません。最初のメッセージでさえ、別のクロスオリジン フレームに「チェーン」されている場合(つまり、最初のレシーバがメッセージを別の宛先に転送した場合)、失敗します。

これは、元の形式とチェーンの両方で、ユーザー アクティベーション v2 で機能します。