メッセージ受け渡し

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

この通信ではメッセージの受け渡しを使用します。これにより、拡張機能とコンテンツ スクリプトの両方が互いのメッセージをリッスンし、同じチャネルで応答できます。メッセージには、有効な JSON オブジェクト(null、ブール値、数値、文字列、配列、オブジェクト)を含めることができます。メッセージ受け渡し API には 2 種類あります。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);
})();

コンテンツ スクリプトにリクエストを送信するには、以下に示すようにリクエストを適用するタブを指定します。この例は、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() を呼び出して拡張機能ページからコンテンツ スクリプトにメッセージを渡します。接続タイプを区別できるようにチャネルに名前を付けることができます。

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

接続を確立すると、その接続を介してメッセージを送受信するための 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 イベントが 1 回呼び出されます。同様に、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 パターンには、少なくとも第 2 レベル ドメインを含める必要があります。つまり、「*」、「*.com」、「*.co.uk」、「*.appspot.com」などのホスト名のパターンはサポートされていません。Chrome 107 以降では、<all_urls> を使用してすべてのドメインにアクセスできます。この脆弱性はすべてのホストに影響するため、Chrome ウェブストアで、この拡張機能を使用している拡張機能の審査に時間がかかることがあります

特定のアプリまたは拡張機能にメッセージを送信するには、runtime.sendMessage() API または 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 API または 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);
  });

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

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

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

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

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

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

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

スクリプトをクロスサイト スクリプティングから保護してください。ユーザー入力など、信頼できないソース(コンテンツ スクリプトや 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;
});