XMLHttpRequest из перекрестного источника

Обычные веб-страницы могут использовать объект XMLHttpRequest для отправки и получения данных с удаленных серверов, но они ограничены политикой одного источника . Скрипты контента инициируют запросы от имени веб-источника, в который они были внедрены, и поэтому на скрипты контента также распространяется политика одного источника . (Скрипты контента подпадают под действие CORB начиная с Chrome 73 и CORS начиная с Chrome 83. ) Источники расширений не имеют таких ограничений — скрипт, выполняющийся на фоновой странице или вкладке на переднем плане расширения, может взаимодействовать с удаленными серверами за пределами своего источника, если расширение запрашивает разрешения на междоменное взаимодействие.

Источник расширения

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

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleStateChange; // Implemented elsewhere.
xhr.open("GET", chrome.extension.getURL('/config_resources/config.json'), true);
xhr.send();

Если расширение попытается использовать источник безопасности, отличный от своего собственного, например, https://www.google.com, браузер запретит это, если расширение не запросило соответствующие разрешения на междоменный доступ.

Запрос разрешений для междоменных запросов

Добавив хосты или шаблоны соответствия хостов (или и то, и другое) в раздел разрешений файла манифеста , расширение может запрашивать доступ к удаленным серверам за пределами своего источника.

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

Значения разрешений для междоменных запросов могут представлять собой полные имена хостов, например, такие:

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

Или это могут быть комбинации, подобные этим:

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

Шаблон соответствия "https://*/" разрешает доступ по HTTPS ко всем доступным доменам. Обратите внимание, что здесь шаблоны соответствия аналогичны шаблонам соответствия скриптов содержимого , но любая информация о пути, следующая за хостом, игнорируется.

Также следует отметить, что доступ предоставляется как по хосту, так и по схеме. Если расширению требуется как безопасный, так и небезопасный HTTP-доступ к данному хосту или набору хостов, оно должно объявить разрешения отдельно:

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

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

Предотвращение уязвимостей межсайтового скриптинга

При использовании ресурсов, полученных через XMLHttpRequest, фоновая страница должна быть осторожна, чтобы не стать жертвой межсайтового скриптинга (XSS) . В частности, избегайте использования опасных API, таких как указанные ниже:

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be evaluating an evil script!
    var resp = eval("(" + xhr.responseText + ")");
    ...
  }
}
xhr.send();
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be injecting a malicious script!
    document.getElementById("resp").innerHTML = xhr.responseText;
    ...
  }
}
xhr.send();

Вместо этого отдавайте предпочтение более безопасным API, которые не запускают скрипты:

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // JSON.parse does not evaluate the attacker's scripts.
    var resp = JSON.parse(xhr.responseText);
  }
}
xhr.send();
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // innerText does not let the attacker inject HTML elements.
    document.getElementById("resp").innerText = xhr.responseText;
  }
}
xhr.send();

Ограничение доступа к скриптам содержимого только для запросов из других источников.

При выполнении междоменных запросов от имени скрипта контента следует проявлять осторожность и защищаться от вредоносных веб-страниц , которые могут попытаться выдать себя за скрипт контента. В частности, не позволяйте скриптам контента запрашивать произвольный 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') {
        var 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

Additionally, be especially careful of resources retrieved via HTTP. If your extension is used on a hostile network, a network attacker (aka a "man-in-the-middle" ) could modify the response and, potentially, attack your extension. Instead, prefer HTTPS whenever possible.

Корректировка политики безопасности контента

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