Usar eval() em iframes em sandbox

O sistema de extensões do Chrome aplica uma Política de Segurança de Conteúdo (CSP) padrão bastante rígida. As restrições de política são diretas: o script precisa ser movido de fora da linha para uma Os arquivos JavaScript e os manipuladores de eventos inline precisam ser convertidos para usar addEventListener, e eval() é desativado.

No entanto, reconhecemos que uma variedade de bibliotecas usam eval() e construções semelhantes a eval, como: new Function() para otimizar o desempenho e facilitar a expressão. As bibliotecas de modelos são especialmente propensos a esse estilo de implementação. Embora alguns (como o Angular.js) ofereçam suporte a CSP de forma nativa, muitos frameworks conhecidos ainda não foram atualizados para um mecanismo compatível com o mundo sem eval das extensões. A remoção do suporte a essa funcionalidade se mostrou mais problemática do que o esperado para os desenvolvedores.

Este documento apresenta o sandbox como um mecanismo seguro para incluir essas bibliotecas nos seus projetos sem comprometer a segurança.

Por que usar o sandbox?

O eval é perigoso em uma extensão porque o código que ele executa tem acesso a tudo no ambiente de permissão alta da extensão. Há uma série de APIs chrome.* poderosas disponíveis que podem afetar gravemente a segurança e a privacidade de um usuário. A extração simples de dados é a menor das nossas preocupações. A solução oferecida é um sandbox em que o eval pode executar o código sem acessar os os dados da extensão ou as APIs de alto valor dela. Sem dados, sem APIs, sem problemas.

Para isso, listamos arquivos HTML específicos no pacote de extensão como sendo sandboxed. Sempre que uma página em sandbox é carregada, ela é movida para uma origem exclusiva e será negada. acesso às APIs chrome.*. Se carregarmos essa página no modo sandbox na nossa extensão usando um iframe, poderemos passá-las, deixar que aja sobre essas mensagens de alguma forma e esperar que ela nos transmita de volta um resultado. Esse mecanismo de mensagens simples oferece tudo o que precisamos para incluir com segurança o código orientado por eval no fluxo de trabalho da extensão.

Criar e usar um sandbox

Se você quiser ir direto ao código, pegue a extensão de exemplo de sandbox e siga desativada. É um exemplo funcional de uma pequena API de mensagens criada sobre os Handlebars (em inglês). de modelos e ela deve fornecer tudo o que você precisa para começar. Para aqueles que um pouco mais de explicação, vamos analisar esse exemplo juntos aqui.

Listar arquivos no manifesto

Cada arquivo que precisa ser executado em um sandbox precisa ser listado no manifesto da extensão adicionando uma propriedade sandbox. Essa é uma etapa importante e fácil de esquecer. Verifique se o arquivo em sandbox está listado no manifesto. Neste exemplo, estamos usando o sandbox do arquivo chamado "sandbox.html". A entrada do manifesto é assim:

{
  ...,
  "sandbox": {
     "pages": ["sandbox.html"]
  },
  ...
}

Carregar o arquivo no sandbox

Para fazer algo interessante com o arquivo em modo de proteção, precisamos fazer o carregamento em um contexto em que ele possa ser tratado pelo código da extensão. Aqui, sandbox.html foi carregado uma página de extensão usando um iframe. O arquivo JavaScript da página contém um código que envia uma mensagem para o sandbox sempre que a ação do navegador é clicada, encontrando o iframe na página e chamando postMessage() no contentWindow. A mensagem é um objeto que contém três propriedades: context, templateName e command. Vamos abordar context e command em breve.

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, '*');
  });

Fazer algo perigoso

Quando o sandbox.html é carregado, ele carrega a biblioteca Handlebars e cria e compila um objeto inline da mesma forma que a Handlebars sugere:

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>

Isso não falha. Embora Handlebars.compile acabe usando new Function, as coisas funcionam exatamente como esperado, e acabamos com um modelo compilado em templates['hello'].

Transmitir o resultado de volta

Vamos disponibilizar esse modelo para uso configurando um listener de mensagens que aceita comandos da página de extensão. Vamos usar o command transmitido para determinar o que precisa ser feito. Você pode imaginar fazer mais do que apenas renderizar, talvez criar modelos? Talvez gerenciá-las de alguma forma o código?), e o context será transmitido diretamente ao modelo para renderização. O HTML renderizado será transmitido de volta à página da extensão para que ela possa fazer algo útil com ele mais tarde:

 <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>

Na página da extensão, vamos receber essa mensagem e fazer algo interessante com os dados html que recebemos. Nesse caso, vamos apenas fazer o eco por meio de uma notificação, mas é totalmente possível usar esse HTML com segurança como parte da interface da extensão. Inserindo-a via O arquivo innerHTML não representa um risco significativo à segurança porque confiamos no conteúdo renderizado. dentro da sandbox.

Esse mecanismo simplifica a criação de modelos, mas ele não se limita a modelos. Qualquer um código que não funciona imediatamente sob uma Política de Segurança de Conteúdo rigorosa pode ser colocado no sandbox; no na verdade, pode ser útil colocar no sandbox componentes das suas extensões que seriam executados corretamente restringir cada parte do programa ao menor conjunto de privilégios necessários para sejam executados corretamente. A apresentação Como criar apps da Web e extensões do Chrome seguros do Google I/O 2012 mostra alguns bons exemplos dessa técnica em ação e vale 56 minutos do seu tempo.