Используйте 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 .

сервис-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 минут вашего времени.