Jogue com segurança em IFrames com sandbox

Criar uma experiência avançada na Web atual quase inevitavelmente envolve a incorporação de componentes e conteúdo sobre os quais você não tem controle real. Os widgets de terceiros podem impulsionar o engajamento e desempenhar um papel fundamental na experiência geral do usuário, e o conteúdo gerado pelo usuário às vezes é ainda mais importante do que o conteúdo nativo de um site. Abster-se de qualquer um deles não é uma opção, mas aumenta o risco de que algo ruim possa acontecer no seu site. Cada widget incorporado, qualquer anúncio ou widget de mídia social, pode ser um vetor de ataque para pessoas com intenção maliciosa:

A Política de Segurança de Conteúdo (CSP) pode reduzir os riscos associados a esses dois tipos de conteúdo, permitindo que você coloque na lista de permissões fontes especificamente confiáveis de scripts e outros conteúdos. Esse é um passo importante na direção certa, mas é importante notar que a proteção que a maioria das diretivas da CSP oferece é binária: o recurso é permitido ou não. Há momentos em que seria útil dizer "Não sei se realmente confio nessa fonte de conteúdo, mas ela é muuuito bonita! Incorpore-o por favor, Navegador, mas não deixe isso corromper meu site."

Privilégio mínimo

Essencialmente, estamos procurando um mecanismo que nos permita conceder conteúdo para incorporar apenas o nível mínimo de capacidade necessário para o trabalho. Se um widget não precisa abrir uma nova janela, remover o acesso a window.open não pode prejudicar. Se ele não exigir o Flash, a desativação do suporte a plug-ins não deve ser um problema. Estaremos o mais seguros possível se seguirmos o princípio de privilégio mínimo e bloquear todos os recursos que não forem diretamente relevantes para a funcionalidade que gostaríamos de usar. O resultado é que não precisamos mais confiar cegamente que algum conteúdo incorporado não aproveitará os privilégios que não deveria usar. Ela simplesmente não terá acesso à funcionalidade.

Os elementos iframe são o primeiro passo em direção a uma boa estrutura para essa solução. Carregar algum componente não confiável em um iframe fornece uma medida de separação entre seu aplicativo e o conteúdo que você quer carregar. O conteúdo em frames não tem acesso ao DOM da página nem aos dados armazenados localmente, nem pode ser desenhado em posições arbitrárias na página. Ele tem escopo limitado ao contorno do frame. No entanto, essa separação não é verdadeiramente robusta. A página contida ainda tem várias opções para comportamentos irritantes ou maliciosos: a reprodução automática de vídeos, plug-ins e pop-ups são a ponta do iceberg.

O atributo sandbox do elemento iframe oferece exatamente o que precisamos para restringir as restrições ao conteúdo em frames. Podemos instruir o navegador a carregar o conteúdo de um frame específico em um ambiente de baixo privilégio, permitindo apenas o subconjunto de recursos necessários para fazer o trabalho necessário.

torça, mas verifique

O botão "Tweet" do Twitter é um ótimo exemplo de funcionalidade que pode ser incorporada com mais segurança ao seu site usando um sandbox. O Twitter permite incorporar o botão por meio de um iframe com o seguinte código:

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

Para descobrir o que podemos bloquear, vamos examinar cuidadosamente quais recursos o botão exige. O HTML carregado no frame executa um pouco de JavaScript nos servidores do Twitter e gera um pop-up preenchido com uma interface de tweet quando clicado. Essa interface precisa acessar os cookies do Twitter para vincular o tweet à conta correta e precisa ter a capacidade de enviar o formulário de tweet. É basicamente isso. O frame não precisa carregar plug-ins, não é necessário navegar pela janela de nível superior ou qualquer um dos vários outros bits de funcionalidade. Como ele não precisa desses privilégios, vamos removê-los colocando o conteúdo do frame no sandbox.

O sandbox funciona com base em uma lista de permissões. Começamos removendo todas as permissões possíveis. Depois, reativamos os recursos individuais adicionando sinalizações específicas à configuração do sandbox. Para o widget do Twitter, decidimos ativar o JavaScript, os pop-ups, o envio de formulários e os cookies do twitter.com. Podemos fazer isso adicionando um atributo sandbox ao iframe com o seguinte valor:

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
    src="https://platform.twitter.com/widgets/tweet_button.html"
    style="border: 0; width:130px; height:20px;"></iframe>

Pronto. Incluímos todos os recursos necessários no frame, e o navegador vai negar o acesso a qualquer um dos privilégios que não concedemos explicitamente pelo valor do atributo sandbox.

Controle granular sobre os recursos

Vimos algumas das possíveis flags de sandbox no exemplo acima. Agora vamos analisar o funcionamento interno do atributo em mais detalhes.

Dado um iframe com um atributo de sandbox vazio, o documento enquadrado será totalmente colocado no sandbox, sujeito às seguintes restrições:

  • O JavaScript não será executado no documento com frame. Isso inclui o JavaScript carregado explicitamente por meio de tags de script, mas também manipuladores de eventos e URLs javascript: inline. Isso também significa que o conteúdo contido em tags noscript será exibido exatamente como se o usuário tivesse desativado o script.
  • O documento em frame é carregado em uma origem exclusiva, o que significa que todas as verificações de mesma origem vão falhar. Origens únicas nunca correspondem a nenhuma outra origem, nem nem a si mesmas. Entre outros impactos, isso significa que o documento não tem acesso aos dados armazenados nos cookies de origem ou em qualquer outro mecanismo de armazenamento (armazenamento DOM, banco de dados indexado etc.).
  • O documento enquadrado não pode criar novas janelas ou caixas de diálogo (via window.open ou target="_blank", por exemplo).
  • Não é possível enviar formulários.
  • Os plug-ins não serão carregados.
  • O documento em frame pode ser acessado apenas por conta própria, e não pelo arquivo pai de nível superior. Definir window.top.location vai gerar uma exceção, e clicar no link com target="_top" não terá efeito.
  • Os recursos acionados automaticamente (elementos de formulário com foco automático, vídeos com reprodução automática etc.) são bloqueados.
  • Não é possível receber o bloqueio do ponteiro.
  • O atributo seamless é ignorado na iframes que o documento enquadrado contém.

Isso é muito draconiano, e um documento carregado em um iframe totalmente colocado no sandbox apresenta muito pouco risco. É claro que isso também não oferece muito valor: você pode usar um sandbox completo para alguns conteúdos estáticos, mas, na maioria das vezes, você vai querer soltar um pouco as coisas.

Com exceção dos plug-ins, cada uma dessas restrições pode ser removida adicionando uma sinalização ao valor do atributo de sandbox. Os documentos no modo sandbox nunca podem executar plug-ins, já que os plug-ins são código nativo fora do sandbox, mas todo o restante é aceitável:

  • allow-forms permite o envio de formulários.
  • O allow-popups permite pop-ups (choque!).
  • O allow-pointer-lock permite o bloqueio de ponteiro (surpresa!).
  • allow-same-origin permite que o documento mantenha a origem. As páginas carregadas de https://example.com/ vão manter o acesso aos dados dessa origem.
  • allow-scripts permite a execução do JavaScript e o acionamento automático dos recursos, já que a implementação deles é fácil usando JavaScript.
  • allow-top-navigation permite que o documento saia do frame navegando pela janela de nível superior.

Com isso em mente, podemos avaliar exatamente por que acabamos com esse conjunto específico de sinalizações de sandbox no exemplo do Twitter acima:

  • O allow-scripts é necessário, porque a página carregada no frame executa um JavaScript para lidar com a interação do usuário.
  • allow-popups é obrigatório, porque o botão mostra um formulário de tweet em uma nova janela.
  • allow-forms é obrigatório, já que o formulário de tweet deve ser enviado.
  • allow-same-origin é necessário, já que os cookies do twitter.com não poderiam ser acessados, e o usuário não poderia fazer login para postar o formulário.

É importante observar que as flags de sandbox aplicadas a um frame também se aplicam a todas as janelas ou frames criados no sandbox. Isso significa que precisamos adicionar allow-forms ao sandbox do frame, mesmo que o formulário exista apenas na janela em que o frame aparece.

Com o atributo sandbox ativado, o widget recebe apenas as permissões necessárias e recursos como plug-ins, navegação na parte de cima e bloqueio de ponteiro permanecem bloqueados. Reduzimos o risco de incorporar o widget, sem efeitos negativos. É uma vitória para todos preocupados.

Separação de privilégios

Colocar conteúdo de terceiros no sandbox para executar códigos não confiáveis em um ambiente de baixo privilégio é bastante útil. Mas e seu próprio código? Você confia em si mesmo, certo? Por que se preocupar com sandbox?

Eu responderia a pergunta: se seu código não precisa de plug-ins, por que dar a ele acesso a plug-ins? Na melhor das hipóteses, é um privilégio que você nunca usa. Na pior, é um vetor em potencial para os invasores entrarem na porta. Todos os códigos têm bugs, e praticamente todos os aplicativos estão vulneráveis à exploração de uma forma ou de outra. Colocar seu código no sandbox significa que, mesmo que um invasor subverta seu aplicativo, ele não vai receber acesso total à origem do aplicativo. Ele só vai conseguir fazer coisas que o aplicativo poderia fazer. Ainda assim, não é tão ruim quanto poderia ser.

É possível reduzir ainda mais o risco dividindo o aplicativo em partes lógicas e colocando cada uma no sandbox com o privilégio mínimo possível. Essa técnica é muito comum no código nativo: o Chrome, por exemplo, se divide em um processo de navegador de alto privilégio que tem acesso ao disco rígido local e pode fazer conexões de rede, e muitos processos de renderizador de baixo privilégios que fazem o trabalho pesado de analisar conteúdo não confiável. Os renderizadores não precisam tocar no disco, o navegador fornece a eles todas as informações necessárias para renderizar uma página. Mesmo que um hacker inteligente encontre uma maneira de corromper um renderizador, ela não chegou muito longe, porque o renderizador não pode fazer muito interesse por conta própria: todo acesso com privilégios elevados precisa ser roteado pelo processo do navegador. Os invasores precisarão encontrar vários buracos em diferentes partes do sistema para fazer qualquer dano, o que reduz enormemente o risco de pwnage bem-sucedida.

Colocar eval() no sandbox com segurança

Com o sandbox e a API postMessage, o sucesso desse modelo é bastante simples de aplicar à Web. As partes do seu aplicativo podem ficar em iframes no modo sandbox e o documento pai pode mediar a comunicação entre elas postando mensagens e detectando respostas. Esse tipo de estrutura garante que as explorações em qualquer parte do app façam o mínimo de dano possível. Ele também tem a vantagem de forçar você a criar pontos de integração claros, para que saiba exatamente onde precisa ter cuidado ao validar a entrada e a saída. Vamos analisar um exemplo de brinquedo, apenas para ver como isso pode funcionar.

O Evalbox é um aplicativo interessante que usa uma string e a avalia como JavaScript. Uau, certo? Era o que você esperava por todos esses anos. É claro que esse é um aplicativo bastante perigoso, já que permitir a execução de JavaScript arbitrário significa que todo e qualquer dado que uma origem tem a oferecer pode ser acessado. Para mitigar o risco de Bad ThingsTM, garante que o código seja executado dentro de um sandbox, o que o torna um pouco mais seguro. Vamos trabalhar no código de dentro para fora, começando pelo conteúdo do frame:

<!-- frame.html -->
<!DOCTYPE html>
<html>
    <head>
    <title>Evalbox's Frame</title>
    <script>
        window.addEventListener('message', function (e) {
        var mainWindow = e.source;
        var result = '';
        try {
            result = eval(e.data);
        } catch (e) {
            result = 'eval() threw an exception.';
        }
        mainWindow.postMessage(result, event.origin);
        });
    </script>
    </head>
</html>

Dentro do frame, temos um documento mínimo que simplesmente ouve mensagens do pai ao se conectar ao evento message do objeto window. Sempre que o pai executa postMessage no conteúdo do iframe, esse evento é acionado, concedendo acesso à string que nosso pai quer executar.

No gerenciador, coletamos o atributo source do evento, que é a janela mãe. Ele será usado para enviar o resultado do nosso trabalho árduo assim que concluí-lo. Em seguida, faremos o trabalho pesado, transmitindo os dados fornecidos para eval(). Essa chamada foi agrupada em um bloco try, já que as operações banidas dentro de um iframe no modo sandbox vão gerar exceções do DOM com frequência. Vamos capturá-las e informar uma mensagem de erro. Por fim, publicamos o resultado de volta na janela pai. Isso é bem objetivo.

O pai é igualmente simples. Vamos criar uma interface pequena com um textarea para o código e um button para execução, e vamos extrair frame.html usando um iframe no modo sandbox, permitindo apenas a execução de scripts:

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

Agora, vamos conectar as coisas para execução. Primeiro, vamos detectar as respostas do iframe e da alert() para os usuários. Presumimos que um aplicativo real faria algo menos irritante:

window.addEventListener('message',
    function (e) {
        // Sandboxed iframes which lack the 'allow-same-origin'
        // header have "null" rather than a valid origin. This means you still
        // have to be careful about accepting data via the messaging API you
        // create. Check that source, and validate those inputs!
        var frame = document.getElementById('sandboxed');
        if (e.origin === "null" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

Em seguida, vamos conectar um manipulador de eventos para cliques no button. Quando o usuário clica, vamos extrair o conteúdo atual do textarea e transmiti-lo ao frame para execução:

function evaluate() {
    var frame = document.getElementById('sandboxed');
    var code = document.getElementById('code').value;
    // Note that we're sending the message to "*", rather than some specific
    // origin. Sandboxed iframes which lack the 'allow-same-origin' header
    // don't have an origin which you can target: you'll have to send to any
    // origin, which might alow some esoteric attacks. Validate your output!
    frame.contentWindow.postMessage(code, '*');
}

document.getElementById('safe').addEventListener('click', evaluate);

Fácil, não é? Criamos uma API de avaliação muito simples e podemos garantir que o código avaliado não tenha acesso a informações confidenciais, como cookies ou armazenamento DOM. Da mesma forma, o código avaliado não pode carregar plug-ins, abrir novas janelas ou qualquer outra atividade irritante ou maliciosa.

É possível fazer o mesmo para seu próprio código, dividindo os aplicativos monolíticos em componentes de finalidade única. Cada um pode ser encapsulado em uma API de mensagens simples, como o que escrevemos acima. A janela mãe de alto privilégio pode atuar como um controlador e agente, enviando mensagens para módulos específicos com os menores privilégios possíveis para realizar o trabalho, detectando resultados e garantindo que cada módulo seja alimentado apenas com as informações necessárias.

No entanto, é necessário ter muito cuidado ao lidar com conteúdo enquadrado que tenha a mesma origem que o pai. Se uma página em https://example.com/ enquadrar outra página da mesma origem com um sandbox que inclui as flags allow-same-origin e allow-scripts, a página com frame pode chegar ao pai e remover o atributo de sandbox totalmente.

Jogar no sandbox

O sandbox está disponível para você agora em vários navegadores: Firefox 17+, IE10+ e Chrome no momento em que este artigo foi escrito (o Caniuse, é claro, tem uma tabela de suporte atualizada). Aplicar o atributo sandbox ao iframes incluído permite conceder determinados privilégios ao conteúdo mostrado, apenas os privilégios necessários para que o conteúdo funcione corretamente. Isso dá a você a oportunidade de reduzir o risco associado à inclusão de conteúdo de terceiros, além do que já é possível com a Política de Segurança de Conteúdo.

Além disso, o sandbox é uma técnica poderosa para reduzir o risco de um invasor inteligente ser capaz de explorar buracos no seu próprio código. Ao separar um aplicativo monolítico em um conjunto de serviços em sandbox, cada um responsável por uma pequena funcionalidade independente, os invasores serão forçados a não apenas comprometer o conteúdo de frames específicos, mas também o controlador deles. Essa é uma tarefa muito mais difícil, especialmente porque o escopo do controlador pode ser muito reduzido. Você pode gastar seu esforço relacionado à segurança auditando esse código se pedir ao navegador para receber ajuda com o restante.

Isso não quer dizer que o sandbox é uma solução completa para o problema de segurança na Internet. Ele oferece defesa em profundidade e, a menos que você tenha controle sobre os clientes dos seus usuários, ainda não é possível contar com o suporte ao navegador para todos os usuários (se você controla os clientes dos seus usuários, um ambiente corporativo, por exemplo, viva!). Algum dia... mas, por enquanto, o sandbox é outra camada de proteção para fortalecer suas defesas, não é uma defesa completa na qual você só pode confiar. Mesmo assim, as camadas são excelentes. Sugiro usar essa.

Leia mais

  • "Separação de privilégios em aplicativos HTML5" (link em inglês) é um documento interessante que funciona por meio do design de um pequeno framework e da aplicação dele a três apps HTML5 existentes.

  • O sandbox pode ser ainda mais flexível quando combinado com dois outros novos atributos de iframe: srcdoc e seamless. O primeiro permite que você preencha um frame com conteúdo sem a sobrecarga de uma solicitação HTTP, e o segundo permite que o estilo flua para o conteúdo em frame. No momento, os dois têm um suporte bastante insatisfatório a navegadores (Chrome e WebKit à noite), mas serão uma combinação interessante no futuro. Você pode, por exemplo, enviar comentários no sandbox de um artigo usando o seguinte código:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>