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

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

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

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