Criar uma experiência rica na Web de hoje quase inevitavelmente envolve incorporar componentes e conteúdo sobre os quais você não tem controle real. Os widgets de terceiros podem aumentar 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 ambos aumentam o risco de que algo ruim possa acontecer no seu site. Cada widget que você incorpora (cada anúncio, cada widget de rede social) é um vetor de ataque potencial para pessoas com intenções maliciosas:
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 script e outros conteúdos. Essa é uma etapa importante na direção certa, mas vale ressaltar que a proteção oferecida pela maioria das diretivas de CSP é binária: o recurso é permitido ou não. Às vezes, seria útil dizer: "Não sei se confio nessa fonte de conteúdo, mas ela é tão bonita! Incorpore-o, por favor, navegador, mas não deixe que ele quebre meu site."
Privilégio mínimo
Em essência, estamos procurando um mecanismo que nos permita conceder ao conteúdo que incorporamos apenas o nível mínimo de capacidade necessário para realizar o trabalho. Se um widget não precisar abrir uma nova janela, remover o acesso a window.open não vai causar problemas. Se não for necessário, desativar o suporte a plug-ins não será um problema. Para garantir o máximo de segurança, siga o princípio de privilégio mínimo e bloqueie todos os recursos que não são diretamente relevantes para a funcionalidade que você quer usar. O resultado é que não precisamos mais confiar cegamente que algum conteúdo incorporado não vai aproveitar privilégios que não deveria usar. Ele simplesmente não terá acesso à funcionalidade.
Os elementos iframe são o primeiro passo para uma boa estrutura de uma solução desse tipo.
Carregar um componente não confiável em um iframe fornece uma medida de separação
entre o aplicativo e o conteúdo que você quer carregar. O conteúdo emoldurado
não terá acesso ao DOM da sua página nem aos dados armazenados localmente, nem poderá
desenhar em posições arbitrárias na página. Ele é limitado ao escopo do
contorno do frame. No entanto, a separação não é realmente robusta. A página contida
ainda tem várias opções de comportamento irritante ou mal-intencionado: 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 aumentar 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 realizar o
trabalho.
Confie, 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 usando 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 com cuidado quais recursos o botão exige. O HTML carregado no frame executa um pouco de JavaScript dos servidores do Twitter e gera um pop-up preenchido com uma interface de tweet quando clicado. Essa interface precisa de acesso aos cookies do Twitter para vincular o tweet à conta correta e precisa da capacidade de enviar o formulário de tweet. É basicamente isso. O frame não precisa carregar plug-ins, navegar na janela de nível superior ou em qualquer outra funcionalidade. Como ele não precisa desses privilégios, vamos removê-los usando o sandbox no conteúdo do frame.
O uso de sandbox funciona com base em uma lista de permissões. Começamos removendo todas as
permissões possíveis e, em seguida, 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. Para isso, adicione 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>
É isso. Demos ao frame todos os recursos necessários, 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 com um pouco mais de detalhes.
Em um iframe com um atributo de sandbox vazio, o documento emoldurado será totalmente colocado em sandbox, sujeitando-o às seguintes restrições:
- O JavaScript não será executado no documento emoldurado. Isso inclui não apenas o JavaScript carregado explicitamente por tags de script, mas também manipuladores de eventos inline e URLs javascript:. 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 exclusivas nunca correspondem a outras origens, nem mesmo a elas mesmas. Entre outros impactos, isso significa que o documento não tem acesso aos dados armazenados nos cookies de qualquer origem ou em qualquer outro mecanismo de armazenamento (armazenamento DOM, IndexedDB etc.).
- O documento em frame não pode criar novas janelas ou caixas de diálogo (por exemplo, usando
window.openoutarget="_blank"). - Não é possível enviar formulários.
- Os plug-ins não serão carregados.
- O documento emoldurado só pode navegar por si mesmo, não pelo pai de nível superior.
Definir
window.top.locationvai gerar uma exceção, e clicar no link comtarget="_top"não terá efeito. - Os recursos que são acionados automaticamente (elementos de formulário com foco automático, vídeos de reprodução automática etc.) são bloqueados.
- Não é possível bloquear o ponteiro.
- O atributo
seamlessé ignorado noiframesque o documento emoldurado contém.
Isso é bastante draconiano, e um documento carregado em um iframe totalmente em sandbox
representa um risco muito pequeno. Claro que ele também não pode fazer muita coisa: talvez
você consiga usar um sandbox completo para algum conteúdo estático, mas na maioria
das vezes você vai querer flexibilizar um pouco as coisas.
Com exceção dos plug-ins, cada uma dessas restrições pode ser suspensa adicionando uma flag ao valor do atributo sandbox. Os documentos em sandbox nunca podem executar plug-ins, já que eles são códigos nativos sem sandbox, mas todo o resto é permitido:
allow-formspermite o envio de formulários.- O
allow-popupspermite pop-ups (surpresa!). allow-pointer-lockpermite (surpresa!) o bloqueio de ponteiro.allow-same-originpermite que o documento mantenha a origem. As páginas carregadas dehttps://example.com/vão manter o acesso aos dados dessa origem.- O
allow-scriptspermite a execução de JavaScript e também que os recursos sejam acionados automaticamente (já que seria simples implementá-los via JavaScript). allow-top-navigationpermite que o documento saia do frame navegando pela janela de nível superior.
Com isso em mente, podemos avaliar exatamente por que acabamos com o conjunto específico de flags de sandbox no exemplo do Twitter acima:
allow-scriptsé necessário, já que o carregamento de página no frame executa um JavaScript para lidar com a interação do usuário.allow-popupsé obrigatório, porque o botão abre um formulário de tweet em uma nova janela.allow-formsé obrigatório, já que o formulário de tweet precisa ser enviado.allow-same-originé necessário, porque, caso contrário, os cookies do twitter.com ficariam inacessíveis, e o usuário não conseguiria 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 que o frame abre.
Com o atributo sandbox, o widget recebe apenas as permissões necessárias, e recursos como plug-ins, navegação superior e bloqueio de ponteiro permanecem bloqueados. Reduzimos o risco de incorporar o widget, sem efeitos negativos.
É uma vitória para todo mundo.
Separação de privilégios
O uso de sandbox em conteúdo de terceiros para executar códigos não confiáveis em um ambiente de baixo privilégio é claramente benéfico. Mas e o seu próprio código? Você confia em si mesmo, certo? Então, por que se preocupar com o sandbox?
Eu faria a pergunta de outra forma: se o código não precisa de plug-ins, por que dar acesso a eles? Na melhor das hipóteses, é um privilégio que você nunca usa. Na pior, é um vetor em potencial para que invasores entrem no sistema. O código de todo mundo tem bugs, e praticamente todos os aplicativos são vulneráveis a explorações de uma forma ou de outra. O sandbox do seu próprio código significa que, mesmo que um invasor consiga subverter seu aplicativo, ele não terá acesso total à origem do aplicativo. Ele só poderá fazer o que o aplicativo pode fazer. Ainda é ruim, mas não tanto quanto poderia ser.
É possível reduzir ainda mais o risco dividindo o aplicativo em partes lógicas e colocando cada uma delas em um sandbox com o mínimo de privilégio possível. Essa técnica é muito comum em 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 renderização de baixo privilégio que fazem o trabalho pesado de analisar conteúdo não confiável. Os renderizadores não precisam tocar no disco, o navegador se encarrega de fornecer todas as informações necessárias para renderizar uma página. Mesmo que um hacker inteligente encontre uma maneira de corromper um renderizador, ele não vai conseguir ir muito longe, já que o renderizador não pode fazer muita coisa por conta própria: todo acesso de alto privilégio precisa ser roteado pelo processo do navegador. Os invasores precisarão encontrar várias falhas em diferentes partes do sistema para causar algum dano, o que reduz significativamente o risco de invasão.
Como usar o sandbox de forma segura eval()
Com o sandbox e a
API postMessage, o
sucesso desse modelo é bastante simples de aplicar à Web. Partes do
seu aplicativo podem residir em iframes em sandbox, e o documento pai pode
intermediar a comunicação entre eles postando mensagens e detectando
respostas. Esse tipo de estrutura garante que as explorações em qualquer parte do
app causem 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 você saiba exatamente onde precisa ter
cuidado ao validar entradas e saídas. Vamos analisar um exemplo simples
para ver como isso funciona.
O Evalbox é um aplicativo interessante que recebe uma string e a avalia como JavaScript. Incrível, né? Exatamente o que você esperava há tantos anos. É claro que esse é um aplicativo bastante perigoso, já que permitir a execução de JavaScript arbitrário significa que todos os dados que uma origem tem a oferecer estão disponíveis. Vamos reduzir o risco de coisas ruins acontecerem garantindo que o código seja executado dentro de um sandbox, o que o torna muito mais seguro. Vamos analisar o 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 detecta mensagens
do pai ao se conectar ao evento message do objeto window.
Sempre que o pai executar postMessage no conteúdo do iframe, esse evento será acionado, acesso à string que o pai quer que executemos.
No gerenciador, pegamos o atributo source do evento, que é a janela
mãe. Vamos usar isso para enviar o resultado do nosso trabalho de volta quando
terminarmos. Em seguida, vamos fazer o trabalho pesado, transmitindo os dados que recebemos para
eval(). Essa chamada foi encapsulada em um bloco try, já que operações proibidas
em um iframe em sandbox costumam gerar exceções de DOM. Vamos capturar
essas exceções e informar uma mensagem de erro amigável. Por fim, postamos o resultado
de volta na janela pai. Isso é bem simples.
O pai também é simples. Vamos criar uma interface pequena com um textarea
para o código e um button para a execução. Além disso, vamos extrair o frame.html usando um
iframe em 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 tudo para a execução. Primeiro, vamos ouvir as respostas do
iframe e alert() para nossos usuários. Presumivelmente, 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" && e.source === frame.contentWindow)
alert('Result: ' + e.data);
});
Em seguida, vamos conectar um manipulador de eventos aos cliques no button. Quando o usuário
clicar, vamos pegar 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 ter certeza de que o código avaliado não tem acesso a informações sensíveis, como cookies ou armazenamento de DOM. Da mesma forma, o código avaliado não pode carregar plug-ins, abrir novas janelas ou realizar outras atividades irritantes ou maliciosas.
Você pode fazer o mesmo com seu próprio código dividindo aplicativos monolíticos em componentes de finalidade única. Cada um pode ser encapsulado em uma API de mensagens simples, como escrevemos acima. A janela pai com privilégios elevados pode atuar como controladora e despachante, enviando mensagens para módulos específicos que têm o mínimo de privilégios possível para realizar as tarefas, detectando resultados e garantindo que cada módulo receba apenas as informações necessárias.
No entanto, é preciso ter muito cuidado ao lidar com conteúdo em frames
que vem da mesma origem que o pai. Se uma página em
https://example.com/ enquadrar outra página na mesma origem com um sandbox
que inclua as flags allow-same-origin e allow-scripts,
a página enquadrada poderá acessar o pai e remover o atributo de sandbox
por completo.
Jogar no sandbox
O sandbox está disponível 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). Ao aplicar o atributo sandbox
ao iframes que você inclui, é possível conceder determinados privilégios ao
conteúdo exibido, apenas os privilégios necessários para que o
conteúdo funcione corretamente. Isso permite 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 conseguir explorar falhas 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 parte da funcionalidade independente, os invasores serão forçados não apenas a 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 ajuda ao navegador com o restante.
Isso não quer dizer que o sandbox seja 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 usuários, ainda não é possível confiar no suporte do navegador para todos os usuários (se você controla os clientes dos usuários, como um ambiente empresarial, por exemplo, parabéns!). Um dia… mas, por enquanto, o sandbox é outra camada de proteção para fortalecer suas defesas, não uma defesa completa em que você pode confiar totalmente. Ainda assim, as camadas são excelentes. Sugiro usar este.
Leitura adicional
"Privilege Separation in HTML5 Applications" é um artigo interessante que aborda o design de um pequeno framework e a aplicação dele a três apps HTML5.
O sandbox pode ser ainda mais flexível quando combinado com dois outros novos atributos de iframe:
srcdoceseamless. O primeiro permite preencher 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 emoldurado. No momento, ambos têm um suporte de navegador bastante ruim (Chrome e WebKit nightlies), mas serão uma combinação interessante no futuro. Por exemplo, é possível colocar comentários em um artigo em um sandbox 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>