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

Обычные веб-страницы могут использовать 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() везде, где это возможно.

Вопросы безопасности

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

При использовании ресурсов, полученных с помощью функции 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 .