Система расширений Chrome применяет довольно строгую политику безопасности контента (CSP) по умолчанию. Ограничения политики просты: сценарий должен быть перемещен вне строки в отдельные файлы JavaScript, встроенные обработчики событий должны быть преобразованы для использования addEventListener
, а eval()
отключен.
Однако мы понимаем, что различные библиотеки используют eval()
и подобные eval
конструкции, такие как new Function()
для оптимизации производительности и простоты выражения. Библиотеки шаблонов особенно склонны к такому стилю реализации. Хотя некоторые (например, Angular.js ) поддерживают CSP «из коробки», многие популярные фреймворки еще не обновились до механизма, совместимого с миром расширений без eval
. Поэтому удаление поддержки этой функциональности оказалось для разработчиков более проблематичным, чем ожидалось .
В этом документе представлена песочница как безопасный механизм включения этих библиотек в ваши проекты без ущерба для безопасности.
Почему песочница?
eval
опасен внутри расширения, поскольку код, который он выполняет, имеет доступ ко всему, что находится в среде расширения с высокими разрешениями. Доступно множество мощных API-интерфейсов chrome.*
, которые могут серьезно повлиять на безопасность и конфиденциальность пользователя; Простая утечка данных — это наименьшее из наших беспокойств. Предлагаемое решение представляет собой «песочницу», в которой eval
может выполнять код без доступа ни к данным расширения, ни к его важным API-интерфейсам. Нет данных, нет API, нет проблем.
Мы достигаем этого, указывая определенные HTML-файлы внутри пакета расширения как изолированные. Всякий раз при загрузке изолированной страницы она будет перемещена в уникальный источник и ей будет отказано в доступе к chrome.*
. Если мы загрузим эту изолированную страницу в наше расширение через iframe
, мы сможем передавать ей сообщения, позволить ей каким-то образом воздействовать на эти сообщения и ждать, пока она вернет нам результат. Этот простой механизм обмена сообщениями дает нам все необходимое для безопасного включения кода, управляемого eval
, в рабочий процесс нашего расширения.
Создайте и используйте песочницу
Если вы хотите сразу погрузиться в код, возьмите образец расширения для песочницы и приступайте к работе . Это рабочий пример крошечного API обмена сообщениями, построенного на основе библиотеки шаблонов Handlebars , и он должен дать вам все необходимое для начала работы. Для тех из вас, кто хочет дополнительных объяснений, давайте вместе пройдемся по этому примеру здесь.
Список файлов в манифесте
Каждый файл, который следует запускать внутри песочницы, должен быть указан в манифесте расширения путем добавления свойства sandbox
. Это важный шаг, и о нем легко забыть, поэтому дважды проверьте, указан ли ваш изолированный файл в манифесте. В этом примере мы помещаем в «песочницу» файл с хитрым названием «sandbox.html». Запись манифеста выглядит следующим образом:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Загрузите файл в песочнице
Чтобы сделать что-то интересное с файлом в песочнице, нам нужно загрузить его в контексте, где к нему может обращаться код расширения. Здесь sandbox.html загружается на страницу расширения через iframe
. Файл javaScript страницы содержит код, который отправляет сообщение в песочницу при каждом нажатии действия браузера путем поиска iframe
на странице и вызова postMessage()
для его contentWindow
. Сообщение представляет собой объект, содержащий три свойства: context
, templateName
и command
. Чуть позже мы углубимся в context
и command
.
сервис-worker.js:
chrome.action.onClicked.addListener(() => {
chrome.tabs.create({
url: 'mainpage.html'
});
console.log('Opened a tab with a sandboxed page!');
});
расширение-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:
расширение-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>
песочница.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
не представляет значительного риска для безопасности, поскольку мы доверяем содержимому, которое было отображено в песочнице.
Этот механизм упрощает создание шаблонов, но, конечно, он не ограничивается шаблонами. Любой код, который не работает «из коробки» в соответствии со строгой политикой безопасности контента, может быть помещен в «песочницу»; на самом деле, часто бывает полезно изолировать компоненты ваших расширений, которые будут работать корректно, чтобы ограничить каждую часть вашей программы наименьшим набором привилегий, необходимых для ее правильного выполнения. Презентация «Написание безопасных веб-приложений и расширений Chrome» на конференции Google I/O 2012 дает несколько хороших примеров использования этих методов в действии и стоит 56 минут вашего времени.