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