В системе расширений Chrome по умолчанию применяется довольно строгая политика защиты контента (CSP).
Ограничения политики просты: скрипты должны быть вынесены в отдельные файлы JavaScript, встроенные обработчики событий должны быть преобразованы для использования addEventListener, а eval() отключен.
Однако мы понимаем, что в различных библиотеках используются конструкции, похожие на eval() и eval, например new Function(), которые позволяют оптимизировать производительность и упростить выражение. Библиотеки шаблонов особенно склонны к такому стилю реализации. Хотя некоторые (например, Angular.js) поддерживают CSP из коробки, многие популярные фреймворки ещё не обновились до механизма, совместимого с миром расширений без eval. Поэтому прекращение поддержки этой функции оказалось более проблематичным, чем ожидалось.
В этом документе описывается изолированная среда как безопасный механизм включения этих библиотек в ваши проекты без ущерба для безопасности.
Зачем нужна изолированная среда?
Использовать eval в расширении опасно, поскольку выполняемый код имеет доступ ко всему, что находится в среде расширения с высоким уровнем разрешений. Доступно множество мощных API browser.*, которые могут серьезно повлиять на безопасность и конфиденциальность пользователя. Простая кража данных – это наименьшая из наших проблем.
Решение, которое мы предлагаем, – это изолированная среда, в которой eval может выполнять код без доступа к данным расширения или его ценным API. Нет данных, нет API, нет проблем.
Для этого мы указываем определенные HTML-файлы в пакете расширения как изолированные.
При загрузке изолированной страницы она будет перемещена в уникальный источник и не сможет получить доступ к API browser.*. Если мы загрузим эту изолированную страницу в наше расширение через iframe, мы сможем передавать ей сообщения, позволять ей каким-то образом реагировать на эти сообщения и ждать, пока она вернет нам результат. Этот простой механизм обмена сообщениями дает нам все необходимое для безопасного включения кода, управляемого eval, в рабочий процесс нашего расширения.
Как создать и использовать тестовую среду
Если вы хотите сразу перейти к коду, возьмите пример расширения для песочницы и взлетайте. Это рабочий пример крошечного API обмена сообщениями, построенного на основе библиотеки шаблонов Handlebars, и он должен дать вам все необходимое для начала работы. Для тех, кто хочет получить более подробное объяснение, давайте вместе рассмотрим этот пример.
Список файлов в манифесте
Каждый файл, который должен запускаться в изолированной среде, необходимо указать в манифесте расширения, добавив свойство sandbox. Это важный шаг, о котором легко забыть, поэтому убедитесь, что файл изолированной среды указан в манифесте. В этом примере мы изолируем файл с названием sandbox.html. Запись в манифесте выглядит следующим образом:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Загрузка файла в изолированной среде
Чтобы сделать что-то интересное с изолированным файлом, нам нужно загрузить его в контекст, где к нему можно обратиться с помощью кода расширения. В этом примере файл sandbox.html загружается на страницу расширения с помощью элемента iframe. Файл JavaScript страницы содержит код, который отправляет сообщение в изолированную среду при каждом нажатии на действие браузера. Для этого он находит на странице элемент iframe и вызывает метод postMessage() для его свойства contentWindow. Сообщение представляет собой объект, содержащий три свойства: context, templateName и command. Мы скоро поговорим о context и command.
service-worker.js:
browser.action.onClicked.addListener(() => {
browser.tabs.create({
url: 'mainpage.html'
});
console.log('Opened a tab with a sandboxed page!');
});
extension-page.js:
let counter = 0;
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('reset').addEventListener('click', function () {
counter = 0;
document.querySelector('#result').innerHTML = '';
});
document.getElementById('sendMessage').addEventListener('click', function () {
counter++;
let message = {
command: 'render',
templateName: 'sample-template-' + counter,
context: { counter: counter }
};
document.getElementById('theFrame').contentWindow.postMessage(message, '*');
});
Совершить опасное действие
При загрузке sandbox.html загружает библиотеку Handlebars, а также создает и компилирует встроенный шаблон, как предлагает Handlebars:
extension-page.html:
<!DOCTYPE html>
<html>
<head>
<script src="mainpage.js"></script>
<link href="styles/main.css" rel="stylesheet" />
</head>
<body>
<div id="buttons">
<button id="sendMessage">Click me</button>
<button id="reset">Reset counter</button>
</div>
<div id="result"></div>
<iframe id="theFrame" src="sandbox.html" style="display: none"></iframe>
</body>
</html>
sandbox.html:
<script id="sample-template-1" type="text/x-handlebars-template">
<div class='entry'>
<h1>Hello</h1>
<p>This is a Handlebar template compiled inside a hidden sandboxed
iframe.</p>
<p>The counter parameter from postMessage() (outer frame) is:
</p>
</div>
</script>
<script id="sample-template-2" type="text/x-handlebars-template">
<div class='entry'>
<h1>Welcome back</h1>
<p>This is another Handlebar template compiled inside a hidden sandboxed
iframe.</p>
<p>The counter parameter from postMessage() (outer frame) is:
</p>
</div>
</script>
Это не может не сработать! Несмотря на то, что Handlebars.compile использует new Function, все работает именно так, как ожидалось, и мы получаем скомпилированный шаблон в templates['hello'].
Передача результата
Чтобы сделать этот шаблон доступным для использования, мы настроим прослушиватель сообщений, который принимает команды со страницы расширения. Мы будем использовать переданный command, чтобы определить, что нужно сделать (вы можете представить, что делаете больше, чем просто рендеринг; возможно, создаете шаблоны? (например, управлять ими), а context будет передаваться непосредственно в шаблон для отрисовки. Обработанный HTML-код будет передан обратно на страницу расширения, чтобы расширение могло сделать с ним что-то полезное позже:
<script>
const templatesElements = document.querySelectorAll(
"script[type='text/x-handlebars-template']"
);
let templates = {},
source,
name;
// precompile all templates in this page
for (let i = 0; i < templatesElements.length; i++) {
source = templatesElements[i].innerHTML;
name = templatesElements[i].id;
templates[name] = Handlebars.compile(source);
}
// Set up message event handler:
window.addEventListener('message', function (event) {
const command = event.data.command;
const template = templates[event.data.templateName];
let result = 'invalid request';
// if we don't know the templateName requested, return an error message
if (template) {
switch (command) {
case 'render':
result = template(event.data.context);
break;
// you could even do dynamic compilation, by accepting a command
// to compile a new template instead of using static ones, for example:
// case 'new':
// template = Handlebars.compile(event.data.templateSource);
// result = template(event.data.context);
// break;
}
} else {
result = 'Unknown template: ' + event.data.templateName;
}
event.source.postMessage({ result: result }, event.origin);
});
</script>
Вернувшись на страницу расширения, мы получим это сообщение и сделаем что-нибудь интересное с переданными нам данными.html В этом случае мы просто выведем его через уведомление, но этот HTML-код вполне можно безопасно использовать как часть интерфейса расширения. Вставка через innerHTML не представляет значительной угрозы безопасности, поскольку мы доверяем контенту, который был обработан в изолированной среде.
Этот механизм упрощает создание шаблонов, но не ограничивается только этим. Любой код, который не работает в строгой Content Security Policy, можно поместить в изолированную среду. На самом деле часто полезно изолировать компоненты расширений, которые будут работать правильно, чтобы ограничить каждую часть программы наименьшим набором привилегий, необходимых для ее правильного выполнения. В презентации Writing Secure Web Apps and Chrome Extensions (Создание безопасных веб-приложений и расширений Chrome) с конференции Google I/O 2012 приведены хорошие примеры использования этих методов. Рекомендуем посмотреть ее.