メッセージ受け渡し

コンテンツ スクリプトは、実行する拡張機能ではなくウェブページのコンテキストで実行されるため、多くの場合、拡張機能の他の部分と通信する方法が必要になります。たとえば、RSS リーダー拡張機能は、コンテンツ スクリプトを使用してページ上の RSS フィードの存在を検出し、そのページのアクション アイコンを表示するようにサービス ワーカーに通知します。

この通信ではメッセージの受け渡しを使用します。これにより、拡張機能とコンテンツ スクリプトの両方が互いのメッセージをリッスンし、同じチャネルで応答できます。メッセージには、有効な JSON オブジェクト(null、ブール値、数値、文字列、配列、オブジェクト)を含めることができます。メッセージ パススルー API は 2 つあります。1 つは1 回限りのリクエスト用で、もう 1 つは複数のメッセージを送信できる長時間接続用です。拡張機能間でのメッセージの送信については、クロス拡張メッセージのセクションをご覧ください。

1 回限りのリクエスト

拡張機能の別の部分に 1 つのメッセージを送信し、必要に応じてレスポンスを取得するには、runtime.sendMessage() または tabs.sendMessage() を呼び出します。これらのメソッドを使用すると、コンテンツ スクリプトから拡張機能、または拡張機能からコンテンツ スクリプトに対して、JSON シリアル化可能な 1 回限りのメッセージを送信できます。レスポンスを処理するには、返された Promise を使用します。古い拡張機能との下位互換性を維持するため、最後の引数としてコールバックを渡すことができます。同じ呼び出しで Promise とコールバックを使用することはできません。

コールバックを Promise に変換する方法と拡張機能で使用する方法については、Manifest V3 移行ガイドをご覧ください。

コンテンツ スクリプトからリクエストを送信するコードは次のようになります。

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

メッセージに同期的に返信する場合は、返信を受け取ったら sendResponse を呼び出し、完了したことを示す false を返します。非同期で応答するには、true を返して、準備が整うまで sendResponse コールバックをアクティブのままにします。非同期関数は、サポートされていない Promise を返すため、サポートされていません。

コンテンツ スクリプトにリクエストを送信するには、以下に示すようにリクエストが適用されるタブを指定します。この例は、Service Worker、ポップアップ、タブとして開いた chrome-extension:// ページで機能します。

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

メッセージを受信するには、runtime.onMessage イベント リスナーをセットアップします。拡張機能とコンテンツ スクリプトの両方で同じコードを使用します。

content-script.js または service-worker.js:

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting === "hello")
      sendResponse({farewell: "goodbye"});
  }
);

上の例では、sendResponse() は同期的に呼び出されました。sendResponse() を非同期で使用するには、onMessage イベント ハンドラに return true; を追加します。

複数のページが onMessage イベントをリッスンしている場合、特定のイベントに対して最初に sendResponse() を呼び出したページのみがレスポンスを送信できます。そのイベントに対する他のすべてのレスポンスは無視されます。

長時間継続する接続

再利用可能な長時間のメッセージ パススルー チャネルを作成するには、runtime.connect() を呼び出してコンテンツ スクリプトから拡張機能ページにメッセージを渡すか、tabs.connect() を呼び出して拡張機能ページからコンテンツ スクリプトにメッセージを渡します。チャンネルに名前を付けて、さまざまなタイプの接続を区別できます。

長時間の接続のユースケースとして、フォーム自動入力拡張機能があります。コンテンツ スクリプトは、特定のログイン用の拡張機能ページへのチャネルを開き、ページ上の入力要素ごとに拡張機能にメッセージを送信して、入力するフォームデータをリクエストします。共有接続により、拡張機能は拡張機能コンポーネント間で状態を共有できます。

接続を確立すると、両端にはその接続を介してメッセージを送受信するための runtime.Port オブジェクトが割り当てられます。

コンテンツ スクリプトからチャネルを開き、メッセージを送受信するには、次のコードを使用します。

content-script.js:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question === "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

拡張機能からコンテンツ スクリプトにリクエストを送信するには、前の例の runtime.connect() の呼び出しを tabs.connect() に置き換えます。

コンテンツ スクリプトまたは拡張機能ページの受信接続を処理するには、runtime.onConnect イベント リスナーを設定します。拡張機能の別の部分が connect() を呼び出すと、このイベントと runtime.Port オブジェクトが有効になります。受信接続に応答するコードは次のようになります。

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer === "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer === "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

ポートの存続期間

ポートは、拡張機能のさまざまな部分間の双方向通信方法として設計されています。最上位フレームは、ポートを使用できる拡張機能の最小部分です。拡張機能の一部が tabs.connect()runtime.connect()、または runtime.connectNative() を呼び出すと、postMessage() を使用してメッセージをすぐに送信できるポートが作成されます。

タブに複数のフレームがある場合、tabs.connect() を呼び出すと、タブ内のフレームごとに runtime.onConnect イベントが呼び出されます。同様に、runtime.connect() が呼び出されると、拡張機能プロセスのフレームごとに onConnect イベントが 1 回発生する可能性があります。

開いているポートごとに個別の状態を維持している場合など、接続が閉じられたタイミングを確認できます。これを行うには、runtime.Port.onDisconnect イベントをリッスンします。このイベントは、チャネルのもう一方の端に有効なポートがない場合に発行されます。これは、次のいずれかの原因が考えられます。

  • 相手側に runtime.onConnect のリスナーがいません。
  • ポートを含むタブがアンロードされる(タブが移動された場合など)。
  • connect() が呼び出されたフレームがアンロードされている。
  • ポートを(runtime.onConnect を介して)受信したすべてのフレームがアンロードされている。
  • runtime.Port.disconnect()相手側によって呼び出されます。connect() 呼び出しの結果、受信側で複数のポートが作成され、これらのポートのいずれかで disconnect() が呼び出された場合は、onDisconnect イベントは送信ポートでのみ発生し、他のポートでは発生しません。

クロス拡張機能メッセージ

拡張機能内の異なるコンポーネント間でメッセージを送信するだけでなく、Messaging API を使用して他の拡張機能と通信することもできます。これにより、他の拡張機能が使用できる公開 API を公開できます。

他の拡張機能からの受信リクエストと接続をリッスンするには、runtime.onMessageExternal メソッドまたは runtime.onConnectExternal メソッドを使用します。それぞれの例を次に示します。

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

別の拡張機能にメッセージを送信するには、通信する拡張機能の ID を次のように渡します。

service-worker.js

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

ウェブページからメッセージを送信する

拡張機能では、他のウェブページからのメッセージの受信や返信も可能ですが、ウェブページへのメッセージの送信はできません。ウェブページから拡張機能にメッセージを送信するには、"externally_connectable" マニフェスト キーを使用して、通信するウェブサイトを manifest.json で指定します。例:

manifest.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

これにより、指定した URL パターンに一致するすべてのページに Messaging API が公開されます。URL パターンには、少なくともセカンダリ ドメインを含める必要があります。つまり、「*」、「*.com」、「*.co.uk」、「*.appspot.com」などのホスト名パターンはサポートされていません。Chrome 107 以降では、<all_urls> を使用してすべてのドメインにアクセスできます。これはすべてのホストに影響するため、これを使用している拡張機能の Chrome ウェブストア審査は時間がかかる場合があります

runtime.sendMessage() または runtime.connect() API を使用して、特定のアプリまたは拡張機能にメッセージを送信します。例:

webpage.js

// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

拡張機能間のメッセージ送信の場合と同様に、拡張機能から runtime.onMessageExternal または runtime.onConnectExternal API を使用してウェブページからのメッセージをリッスンします。次の例をご覧ください。

service-worker.js

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

ネイティブ メッセージング

拡張機能は、ネイティブ メッセージング ホストとして登録されているネイティブ アプリケーションとメッセージを交換できます。この機能の詳細については、ネイティブ メッセージングをご覧ください。

セキュリティ上の考慮事項

メッセージングに関連するセキュリティ上の考慮事項をいくつか紹介します。

コンテンツ スクリプトの信頼性が低い

コンテンツ スクリプトは、拡張機能のサービス ワーカーよりも信頼性が低い。たとえば、悪意のあるウェブページによって、コンテンツ スクリプトを実行するレンダリング プロセスが侵害される可能性があります。コンテンツ スクリプトからのメッセージが攻撃者によって作成された可能性があると想定し、すべての入力の検証とサニタイズを行ってください。コンテンツ スクリプトに送信されたデータがウェブページに漏洩する可能性があると想定します。コンテンツ スクリプトから受信したメッセージによってトリガーされる特権アクションのスコープを制限します。

クロスサイト スクリプティング

スクリプトをクロスサイト スクリプティングから保護してください。ユーザー入力、コンテンツ スクリプト経由の他のウェブサイト、API などの信頼できないソースからデータを受信する場合は、HTML として解釈したり、予期しないコードの実行を許可する方法で使用したりしないように注意してください。

より安全な方法

可能な限り、スクリプトを実行しない API を使用してください。

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
安全でないメソッド

拡張機能を脆弱にする次の方法は使用しないでください。

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});