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 rigorosa. As restrições da política são diretas: o script precisa ser movido fora da linha para arquivos JavaScript separados, os manipuladores de eventos inline precisam ser convertidos para usar o addEventListener e o eval() é desativado.

No entanto, sabemos que várias bibliotecas usam construções semelhantes a eval() e eval, como new Function(), para otimização do desempenho e facilidade de expressão. As bibliotecas de modelos são especialmente propensas a esse estilo de implementação. Embora alguns frameworks (como o Angular.js) sejam compatíveis com a CSP, muitos frameworks conhecidos ainda não foram atualizados para um mecanismo compatível com o mundo sem eval das extensões. Portanto, a remoção do suporte para essa funcionalidade se mostrou mais problemático do que o esperado para os desenvolvedores.

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

Por que usar o sandbox?

eval é perigoso dentro de uma extensão porque o código que ele executa tem acesso a tudo no ambiente de alta permissão da extensão. Diversas APIs chrome.* eficientes estão disponíveis e poderiam ter um impacto grave na segurança e na privacidade do usuário. A simples exfiltração de dados é a menor das nossas preocupações. A solução oferecida é um sandbox em que eval pode executar código sem acesso aos dados da extensão ou às APIs de alto valor dela. Sem dados, sem APIs, sem problemas.

Fazemos isso listando arquivos HTML específicos dentro do pacote de extensões como se estivessem em sandbox. Sempre que uma página no modo sandbox é carregada, ela é movida para uma origem exclusiva e tem acesso negado a APIs chrome.*. Se carregarmos essa página no modo sandbox na nossa extensão usando um iframe, poderemos transmitir mensagens a ela, deixar que ela aja de acordo com essas mensagens de alguma forma e esperar que ela nos envie um resultado. Esse mecanismo de mensagens simples oferece tudo o que precisamos para incluir com segurança um código orientado por eval no fluxo de trabalho da nossa extensão.

Criar e usar um sandbox

Se você quiser ir direto ao código, pegue a extensão de exemplo de sandbox e decole. É um exemplo funcional de uma pequena API de mensagens criada com base na biblioteca de modelos Handlebars, e fornece tudo o que você precisa para começar. Para aqueles que gostariam de um pouco mais de explicação, vamos analisar essa amostra juntos aqui.

Listar arquivos no manifesto

Cada arquivo que precisa ser executado em um sandbox precisa ser listado no manifesto de extensão adicionando uma propriedade sandbox. Essa é uma etapa crítica e fácil de esquecer. Por isso, confira se o arquivo no modo sandbox está listado no manifesto. Neste exemplo, colocamos o arquivo no sandbox chamado de "sandbox.html". A entrada do manifesto tem esta aparência:

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

Carregar o arquivo no modo sandbox

Para fazer algo interessante com o arquivo no modo sandbox, precisamos carregá-lo em um contexto em que ele possa ser resolvido pelo código da extensão. Aqui, o sandbox.html foi carregado em uma página de extensão por 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 nos aprofundar em 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 sandbox.html é carregado, ele carrega a biblioteca Handlebars, além de criar e compilar um modelo in-line da maneira sugerida:

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 vai falhar. Mesmo que Handlebars.compile acabe usando new Function, tudo funciona exatamente como esperado, e acabamos com um modelo compilado em templates['hello'].

Transmitir o resultado de volta

Disponibilizaremos esse modelo para uso configurando um listener de mensagens que aceite comandos da página de extensões. Vamos usar o command transmitido para determinar o que precisa ser feito. Você poderia imaginar fazer mais do que simplesmente renderizar, talvez criando modelos? talvez gerenciá-los de alguma maneira?), e o context será transmitido diretamente ao modelo para renderização. O HTML renderizado será retornado à página da extensão para que a extensão 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>

De volta à página da extensão, receberemos essa mensagem e faremos algo interessante com os dados do html que recebemos. Nesse caso, faremos o echo usando uma notificação, mas é possível usar esse HTML com segurança como parte da interface da extensão. Inserir o arquivo usando innerHTML não representa um risco de segurança significativo, porque confiamos no conteúdo renderizado no sandbox.

Esse mecanismo torna os modelos simples, mas não se limita a eles. Qualquer código que não funcione de acordo com uma Política de Segurança de Conteúdo estrita pode ser colocado no sandbox. Na verdade, muitas vezes é útil colocar no sandbox os componentes das suas extensões que seriam executados corretamente para restringir cada parte do programa ao menor conjunto de privilégios necessários para que ele seja executado corretamente. A apresentação Como criar apps da Web seguros e extensões do Chrome (em inglês) do Google I/O 2012 mostra alguns bons exemplos dessa técnica em ação e vale 56 minutos do seu tempo.