Используйте eval() в изолированных iframe

Система расширений 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 чуть позже.

service-worker.js:

chrome.action.onClicked.addListener(() => {
  chrome.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 не представляет существенного риска для безопасности, поскольку мы доверяем контенту, который был отображен в песочнице.

Этот механизм упрощает создание шаблонов, но, конечно, он не ограничивается только ими. Любой код, который не работает «из коробки» при строгой политике безопасности контента, можно изолировать; на самом деле, часто полезно изолировать компоненты ваших расширений, которые будут работать корректно, чтобы ограничить каждую часть вашей программы минимальным набором привилегий, необходимых для ее правильного выполнения. Презентация « Создание безопасных веб-приложений и расширений Chrome» с Google I/O 2012 содержит несколько хороших примеров применения этих методов на практике и стоит потраченных 56 минут вашего времени.