クロスオリジン ネットワーク リクエスト

通常のウェブページでは、fetch() API または XMLHttpRequest API を使用してリモート サーバーとの間でデータを送受信できますが、同一生成元ポリシーによって制限されます。コンテンツ スクリプトは、挿入先のウェブにおけるオリジンに代わってリクエストを開始するため、コンテンツ スクリプトにも同一生成元ポリシーが適用されます。拡張機能の生成元には、このような制限はありません。拡張機能の Service Worker またはフォアグラウンド タブで実行されるスクリプトは、拡張機能がホスト権限をリクエストしている限り、そのオリジン以外のリモート サーバーと通信できます。

拡張機能の生成元

実行中の各拡張機能は、独自のセキュリティ生成元内に存在します。追加の権限をリクエストしなくても、拡張機能は fetch() を呼び出してインストール内のリソースを取得できます。たとえば、拡張機能に config_resources/ フォルダに config.json という JSON 構成ファイルが含まれている場合、拡張機能は次のようにしてファイルの内容を取得できます。

const response = await fetch('/config_resources/config.json');
const jsonData = await response.json();

拡張機能が独自のセキュリティ生成元以外の生成元(https://www.google.com など)からコンテンツをリクエストしようとすると、拡張機能にホスト権限がない限り、クロスオリジン リクエストとして扱われます。拡張機能にホスト権限がある場合でも、クロスオリジン リクエストは常にコンテンツ スクリプトでそのように扱われます。

クロスオリジン権限をリクエストする

拡張機能の生成元外のリモート サーバーへのアクセスをリクエストするには、マニフェスト ファイルの host_permissions セクションにホスト、マッチパターン、 またはその両方を追加します。

{
  "name": "My extension",
  ...
  "host_permissions": [
    "https://www.google.com/"
  ],
  ...
}

クロスオリジン権限の値には、次のような完全修飾ホスト名を使用できます。

  • "https://www.google.com/"
  • "https://www.gmail.com/"

または、次のようなマッチパターンを使用できます。

  • "https://*.google.com/"
  • "https://*/"

マッチパターン「https://*/」を使用すると、到達可能なすべてのドメインへの HTTPS アクセスが可能になります。なお、マッチ パターンはコンテンツ スクリプトのマッチパターンと似ていますが、ホストの後のパス情報は無視されます。

また、アクセス権はホストとスキームの両方で付与されます。拡張機能が特定のホストまたはホストのセットに対して安全な HTTP アクセスと安全でない HTTP アクセスの両方を必要とする場合は、権限を個別に宣言する必要があります。

"host_permissions": [
  "http://www.google.com/",
  "https://www.google.com/"
]

fetch() と XMLHttpRequest()

fetch() はサービス ワーカー専用に作成されたものであり、同期オペレーションから離れるというウェブのトレンドに沿っています。XMLHttpRequest() API は Service Worker 以外の拡張機能でサポートされており、この API を呼び出すと拡張機能の Service Worker の fetch ハンドラがトリガーされます。新しい作業では、可能な限り fetch() を使用してください。

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

クロスサイト スクリプティングの脆弱性を回避する

fetch() で取得したリソースを使用する場合は、オフスクリーン ドキュメント、サイドパネル、ポップアップがクロスサイト スクリプティングの被害に遭わないように注意する必要があります。特に、innerHTML などの危険な API の使用は避けてください。次に例を示します。

const response = await fetch("https://api.example.com/data.json");
const jsonData = await response.json();
// WARNING! Might be injecting a malicious script!
document.getElementById("resp").innerHTML = jsonData;
    ...

代わりに、スクリプトを実行しない安全な API を使用してください。

const response = await fetch("https://api.example.com/data.json");
const jsonData = await response.json();
// JSON.parse does not evaluate the attacker's scripts.
let resp = JSON.parse(jsonData);

const response = await fetch("https://api.example.com/data.json");
const jsonData = response.json();
// textContent does not let the attacker inject HTML elements.
document.getElementById("resp").textContent = jsonData;

コンテンツ スクリプトのクロスオリジン リクエストへのアクセスを制限する

コンテンツ スクリプトに代わってクロスオリジン リクエストを実行する場合は、コンテンツ スクリプトを偽装しようとする悪意のあるウェブページ から保護するように注意してください。特に、コンテンツ スクリプトが任意の URL をリクエストすることを許可しないでください。

拡張機能がクロスオリジン リクエストを実行して、コンテンツ スクリプトがアイテムの価格を検出できるようにする例を考えてみましょう。あまり安全ではない方法としては、コンテンツ スクリプトに、バックグラウンド ページで取得する正確なリソースを指定させる方法があります。

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.contentScriptQuery == 'fetchUrl') {
      // WARNING: SECURITY PROBLEM - a malicious web page may abuse
      // the message handler to get access to arbitrary cross-origin
      // resources.
      fetch(request.url)
        .then(response => response.text())
        .then(text => sendResponse(text))
        .catch(error => ...)
      return true;  // Will respond asynchronously.
    }
  }
);
chrome.runtime.sendMessage(
  {
    contentScriptQuery: 'fetchUrl',
    url: `https://another-site.com/price-query?itemId=${encodeURIComponent(request.itemId)}`
  },
  response => parsePrice(response.text())
);

上記の方法では、コンテンツ スクリプトは、拡張機能がアクセスできる任意の URL を取得するように拡張機能に要求できます。悪意のあるウェブページは、このようなメッセージを偽造して、拡張機能にクロスオリジン リソースへのアクセスを許可させることができます。

代わりに、取得できるリソースを制限するメッセージ ハンドラを設計してください。以下では、コンテンツ スクリプトから提供されるのは itemId のみで、完全な URL は提供されません。

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.contentScriptQuery == 'queryPrice') {
      const url = `https://another-site.com/price-query?itemId=${encodeURIComponent(request.itemId)}`
      fetch(url)
        .then(response => response.text())
        .then(text => parsePrice(text))
        .then(price => sendResponse(price))
        .catch(error => ...)
      return true;  // Will respond asynchronously.
    }
  }
);
chrome.runtime.sendMessage(
  {contentScriptQuery: 'queryPrice', itemId: 12345},
  price => ...
);

HTTP より HTTPS を優先する

また、HTTP で取得したリソースには特に注意してください。拡張機能が 敵対的なネットワークで使用されている場合、ネットワーク攻撃者("man-in-the-middle")がレスポンスを 変更し、拡張機能を攻撃する可能性があります。可能な限り HTTPS を使用してください。

コンテンツ セキュリティ ポリシーを調整する

マニフェストに content_security_policy属性を追加して、拡張機能のデフォルトのコンテンツ セキュリティ ポリシーを変更する場合は、接続先のホストが許可されていることを確認する必要があります。デフォルトのポリシーではホストへの接続は制限されませんが、connect-src ディレクティブまたは default-src ディレクティブを明示的に追加する場合は注意してください。