Сетевые запросы между источниками

Обычные веб-страницы могут использовать API-интерфейсы fetch() или XMLHttpRequest для отправки и получения данных с удаленных серверов, но они ограничены одной и той же политикой происхождения . Скрипты контента инициируют запросы от имени веб-источника, в который был внедрен скрипт контента, и поэтому сценарии контента также подчиняются той же политике происхождения . Происхождение расширений не так ограничено. Сценарий, выполняющийся в работнике службы расширений или на вкладке переднего плана, может взаимодействовать с удаленными серверами за пределами его источника, если расширение запрашивает разрешения между источниками.

Происхождение расширения

Каждое работающее расширение существует в своем отдельном источнике безопасности. Не запрашивая дополнительных привилегий, расширение может вызвать fetch() для получения ресурсов в рамках своей установки. Например, если расширение содержит файл конфигурации JSON с именем config.json в папке config_resources/ , расширение может получить содержимое файла следующим образом:

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-доступ к данному хосту или набору хостов, оно должно объявить разрешения отдельно:

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

Fetch() против XMLHttpRequest()

fetch() был создан специально для сервисных работников и следует более широкой тенденции в Интернете, отходящей от синхронных операций. API XMLHttpRequest() поддерживается в расширениях вне сервисного работника, и его вызов запускает обработчик выборки сервисного работника расширения. В новой работе везде, где это возможно, следует отдавать предпочтение fetch() .

Соображения безопасности

Избегайте уязвимостей межсайтового скриптинга

При использовании ресурсов, полученных с помощью fetch() , ваш закадровый документ, боковая панель или всплывающее окно должны быть осторожны, чтобы не стать жертвой межсайтового скриптинга . В частности, избегайте использования опасных API, таких как innerHTML . Например:

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 => ...
);

Предпочитайте HTTPS HTTP

Кроме того, будьте особенно осторожны с ресурсами, полученными через HTTP. Если ваше расширение используется во враждебной сети, сетевой злоумышленник (также известный как «человек посередине» ) может изменить ответ и, возможно, атаковать ваше расширение. Вместо этого, когда это возможно, отдавайте предпочтение HTTPS.

Настройте политику безопасности контента

Если вы измените политику безопасности контента по умолчанию для своего расширения, добавив атрибут content_security_policy в свой манифест, вам необходимо убедиться, что все хосты, к которым вы хотите подключиться, разрешены. Хотя политика по умолчанию не ограничивает подключения к хостам, будьте осторожны при явном добавлении директив connect-src или default-src .

,

Обычные веб-страницы могут использовать API-интерфейсы fetch() или XMLHttpRequest для отправки и получения данных с удаленных серверов, но они ограничены одной и той же политикой происхождения . Скрипты контента инициируют запросы от имени веб-источника, в который был внедрен скрипт контента, и поэтому сценарии контента также подчиняются той же политике происхождения . Происхождение расширений не так ограничено. Сценарий, выполняющийся в работнике службы расширений или на вкладке переднего плана, может взаимодействовать с удаленными серверами за пределами его источника, если расширение запрашивает разрешения между источниками.

Происхождение расширения

Каждое работающее расширение существует в своем отдельном источнике безопасности. Не запрашивая дополнительных привилегий, расширение может вызвать fetch() для получения ресурсов в рамках своей установки. Например, если расширение содержит файл конфигурации JSON с именем config.json в папке config_resources/ , расширение может получить содержимое файла следующим образом:

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-доступ к данному хосту или набору хостов, оно должно объявить разрешения отдельно:

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

Fetch() против XMLHttpRequest()

fetch() был создан специально для сервисных работников и следует более широкой тенденции в Интернете, отходящей от синхронных операций. API XMLHttpRequest() поддерживается в расширениях вне сервисного работника, и его вызов запускает обработчик выборки сервисного работника расширения. В новой работе везде, где это возможно, следует отдавать предпочтение fetch() .

Соображения безопасности

Избегайте уязвимостей межсайтового скриптинга

При использовании ресурсов, полученных с помощью fetch() , ваш закадровый документ, боковая панель или всплывающее окно должны быть осторожны, чтобы не стать жертвой межсайтового скриптинга . В частности, избегайте использования опасных API, таких как innerHTML . Например:

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 => ...
);

Предпочитайте HTTPS HTTP

Кроме того, будьте особенно осторожны с ресурсами, полученными через HTTP. Если ваше расширение используется во враждебной сети, сетевой злоумышленник (также известный как «человек посередине» ) может изменить ответ и, возможно, атаковать ваше расширение. Вместо этого, когда это возможно, отдавайте предпочтение HTTPS.

Настройте политику безопасности контента

Если вы измените политику безопасности контента по умолчанию для своего расширения, добавив атрибут content_security_policy в свой манифест, вам необходимо убедиться, что все хосты, к которым вы хотите подключиться, разрешены. Хотя политика по умолчанию не ограничивает подключения к хостам, будьте осторожны при явном добавлении директив connect-src или default-src .