Criar uma experiência rica na Web atual quase sempre envolve a incorporação de 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. Além disso, o conteúdo gerado pelo usuário às vezes é ainda mais importante do que o conteúdo nativo de um site. Não fazer isso não é uma opção, mas ambos aumentam o risco de que algo ruim™ aconteça no seu site. Cada widget incorporado, como anúncios e widgets de mídias sociais, é um possível vetor de ataque 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 adicionar à lista de permissões fontes de script e outros conteúdos confiáveis. Essa é uma etapa importante na direção certa, mas vale ressaltar que a proteção oferecida pela maioria das diretivas do CSP é binária: o recurso é permitido ou não. Às vezes, é útil dizer "Não tenho certeza se confio nessa fonte de conteúdo, mas ela é muito legal. 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 permita conceder ao conteúdo incorporado apenas o nível mínimo de capacidade necessário para fazer o trabalho. Se um widget não precisa abrir uma nova janela, remover o acesso a window.open não faz mal. Se não for necessário usar o Flash, desativar o suporte a plug-ins não será um problema. A segurança é máxima se seguirmos o princípio do menor privilégio e bloquearmos todos os recursos que não são diretamente relevantes para a funcionalidade que queremos usar. O resultado é que não precisamos mais confiar cegamente que um pedaço de conteúdo incorporado não vai aproveitar privilégios que não deveria. Ele simplesmente não terá acesso à funcionalidade.
Os elementos iframe
são a primeira etapa para uma boa estrutura para essa solução.
O carregamento de um componente não confiável em uma 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 página ou aos dados armazenados localmente, nem
poderá ser exibido em posições arbitrárias na página. Ele é limitado ao
desenho do frame. No entanto, a separação não é realmente robusta. A página contida
ainda tem várias opções para comportamento irritante ou malicioso: vídeos
automáticos, plug-ins e pop-ups são apenas a ponta do iceberg.
O atributo sandbox
do elemento iframe
nos dá exatamente o que precisamos para aumentar as restrições ao conteúdo em moldura. Podemos
instruir o navegador a carregar o conteúdo de um frame específico em um ambiente de
privilégios baixos, permitindo apenas o subconjunto de recursos necessários para fazer o
trabalho necessário.
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 cuidadosamente os recursos necessários para o botão. 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 ter acesso aos cookies do Twitter para vincular o tweet à conta correta e enviar o formulário de tweet. É isso. O frame não precisa carregar nenhum plug-in, navegar pela janela de nível superior ou qualquer outro pequeno pedaço de funcionalidade. Como ele não precisa desses privilégios, vamos removê-los usando sandbox no conteúdo do frame.
O 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
flags 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 fazer 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
foram concedidos explicitamente pelo valor do atributo sandbox
.
Controle granular de recursos
Vimos algumas das possíveis flags de sandbox no exemplo acima. Vamos analisar o funcionamento interno do atributo com um pouco mais de detalhes.
Se um iframe tiver um atributo de sandbox vazio, o documento enquadrado será totalmente isolado, estando sujeito a estas restrições:
- O JavaScript não será executado no documento emoldurado. Isso não inclui apenas JavaScript carregado explicitamente por tags de script, mas também gerenciadores de eventos inline e URLs javascript:. Isso também significa que o conteúdo contido nas tags noscript será exibido, exatamente como se o usuário tivesse desativado o script.
- O documento emoldurado é carregado em uma origem exclusiva, o que significa que todas as verificações de mesma origem vão falhar. Origens exclusivas não correspondem a nenhuma outra origem, nem mesmo a si mesmas. Entre outros impactos, isso significa que o documento não tem acesso aos dados armazenados nos cookies de qualquer origem ou em outros mecanismos de armazenamento (armazenamento DOM, DB indexado etc.).
- O documento enquadrado não pode criar novas janelas ou caixas de diálogo (por exemplo, usando
window.open
outarget="_blank"
). - Não é possível enviar formulários.
- Os plug-ins não são carregados.
- O documento enquadrado só pode navegar em si mesmo, não no pai de nível superior.
A configuração de
window.top.location
gera uma exceção, e clicar no link comtarget="_top"
não tem efeito. - Os recursos que são acionados automaticamente (elementos de formulário com foco automático, vídeos em reprodução automática etc.) são bloqueados.
- Não foi possível bloquear o ponteiro.
- O atributo
seamless
é ignorado noiframes
que o documento emoldurado contém.
Isso é bastante rigoroso, e um documento carregado em um iframe
totalmente em modo sandbox
apresenta muito pouco risco. É claro, isso também não tem muito valor. Você
pode conseguir uma sandbox completa para algum conteúdo estático, mas, na maioria
das vezes, é melhor relaxar um pouco.
Com exceção dos plug-ins, cada uma dessas restrições pode ser suspensa adicionando uma flag ao valor do atributo do sandbox. Documentos em sandbox nunca podem executar plug-ins, porque eles são códigos nativos sem sandbox, mas tudo o mais é permitido:
allow-forms
permite o envio de formulários.allow-popups
permite (surpresa!) pop-ups.allow-pointer-lock
permite (surpresa!) o bloqueio do ponteiro.allow-same-origin
permite que o documento mantenha sua origem. As páginas carregadas dehttps://example.com/
vão manter o acesso aos dados dessa origem.allow-scripts
permite a execução do JavaScript e também permite que os recursos sejam acionados automaticamente, já que a implementação deles pelo JavaScript é simples.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 o conjunto específico de flags de sandbox no exemplo do Twitter acima:
- O
allow-scripts
é necessário, já que a página carregada no frame executa alguns JavaScripts para lidar com a interação do usuário. - O
allow-popups
é necessário, porque o botão abre um formulário de tweet em uma nova janela. allow-forms
é obrigatório, porque o formulário de tweets precisa ser enviado.- O
allow-same-origin
é necessário, porque, caso contrário, os cookies do twitter.com ficariam inacessíveis, 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 só exista
na janela em que o frame aparece.
Com o atributo sandbox
em vigor, o widget recebe apenas as permissões
necessárias, e recursos como plug-ins, navegação superior e bloqueio do ponteiro permanecem
bloqueados. Reduzimos o risco de incorporação do widget, sem efeitos adversos.
É uma vitória para todos os envolvidos.
Separação de privilégios
O sandboxing de conteúdo de terceiros para executar o código não confiável em um ambiente de privilégios baixo é bastante benéfico. Mas e o seu código? Você confia em si mesmo, certo? Então, por que se preocupar com o sandbox?
Eu inverteria essa pergunta: se o 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 potencial para invasores. O código de todo mundo tem bugs, e praticamente todos os aplicativos são vulneráveis a exploração de uma forma ou de outra. O sandbox do seu próprio código significa que, mesmo que um invasor subverta seu aplicativo, ele não terá acesso completo à origem do aplicativo. Ele só poderá fazer o que o aplicativo pode fazer. Ainda é ruim, mas não tão ruim quanto poderia ser.
É possível reduzir ainda mais o risco dividindo o aplicativo em partes lógicas e usando o sandbox em cada parte 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 privilégios altos que tem acesso ao disco rígido local e pode fazer conexões de rede, e muitos processos de renderizador de privilégios baixos que fazem o trabalho pesado de analisar conteúdo não confiável. Os renderizadores não precisam tocar no disco. O navegador cuida de fornecer 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, ele não vai conseguir ir muito longe, já que o renderizador não pode fazer muito por conta própria: todo acesso de alto privilégio precisa ser roteado pelo processo do navegador. Os invasores precisam encontrar vários buracos em diferentes partes do sistema para causar danos, o que reduz bastante o risco de pwnage.
Sandbox de segurança para eval()
Com o sandboxing e a
API postMessage
, o
sucesso desse modelo é bastante simples de aplicar na Web. Partes
do seu aplicativo podem ficar em iframe
s em modo sandbox, e o documento pai pode
mediar a comunicação entre elas postando mensagens e detectando
respostas. Esse tipo de estrutura garante que os exploits em qualquer parte do
app causem o mínimo de danos possível. Ela 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 a entrada e a saída. Vamos usar um exemplo de brinquedo
para entender como isso funciona.
O Evalbox é um aplicativo interessante que recebe uma string e a avalia como JavaScript. Uau, né? Exatamente o que você estava esperando há todos esses anos. É uma aplicação bastante perigosa, é claro, porque permitir a execução de JavaScript arbitrário significa que todos os dados que uma origem tem para oferecer estão disponíveis. Vamos reduzir o risco de coisas ruins™ acontecendo, garantindo que o código seja executado em um sandbox, o que o torna muito 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 detecta mensagens
do pai, conectando-se 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
executamos.
No gerenciador, extraímos o atributo source
do evento, que é a janela
mãe. Vamos usar isso para enviar o resultado do nosso trabalho quando terminarmos. Em seguida, vamos fazer o trabalho pesado, transmitindo os dados que recebemos para
eval()
. Essa chamada foi embrulhada em um bloco try, porque operações proibidas
dentro de um iframe
em sandbox geralmente geram exceções DOM. Vamos capturar
essas exceções e informar uma mensagem de erro amigável. Por fim, postamos o resultado
de volta para a janela pai. Isso é bem simples.
O pai é igualmente simples. Vamos criar uma pequena interface com um textarea
para o código e um button
para a execução. Em seguida, vamos extrair frame.html
usando um
iframe
em modo de proteção, permitindo apenas a execução do script:
<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 a 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 DOM. Da mesma forma, o código avaliado não pode carregar plug-ins, abrir novas janelas ou realizar qualquer outra atividade irritante ou maliciosa.
Você pode fazer o mesmo com seu próprio código dividindo aplicativos monolíticos em componentes de finalidade única. Cada uma pode ser agrupada em uma API de mensagens simples, assim como escrevemos acima. A janela pai de alto privilégio pode atuar como controlador e despachante, enviando mensagens para módulos específicos que têm o menor número possível de privilégios para fazer o trabalho, detectando resultados e garantindo que cada módulo seja alimentado com apenas as informações necessárias.
No entanto, é preciso ter muito cuidado ao lidar com conteúdo em moldura
que venha da mesma origem que o principal. 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á alcançar a página mãe e remover o atributo sandbox
inteiramente.
Testar no sandbox
O sandboxing já está disponível em vários navegadores: Firefox 17 e versões mais recentes,
IE 10 e versões mais recentes e Chrome no momento da escrita (o caniuse, é claro, tem uma tabela de suporte
atualizada). Aplicar o atributo sandbox
ao iframes
que você inclui permite conceder determinados privilégios ao
conteúdo exibido, apenas aqueles que são 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 sandboxing é uma técnica poderosa para reduzir o risco de que um invasor esperto consiga 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 um pequeno trecho de funcionalidade independente, os invasores são forçados a não comprometer apenas o conteúdo de frames específicos, mas também o controlador deles. Essa é uma tarefa muito mais difícil, especialmente porque o controlador pode ser bastante reduzido no escopo. Você pode usar seu esforço relacionado à segurança para auditar esse código se pedir ajuda ao navegador para o restante.
Isso não significa que o sandboxing 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 contar com o suporte do navegador para todos os usuários. Se você controla os clientes dos usuários, como um ambiente corporativo, 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ê possa confiar. Ainda assim, as camadas são excelentes. Sugiro usar este link.
Leitura adicional
Separação de privilégios em aplicativos HTML5 é um artigo interessante que aborda o design de uma pequena estrutura e sua aplicação em três apps HTML5.
O sandbox pode ser ainda mais flexível quando combinado com dois outros novos atributos de iframe:
srcdoc
eseamless
. A primeira permite preencher um frame com conteúdo sem a sobrecarga de uma solicitação HTTP, e a segunda permite que o estilo flua para o conteúdo enquadrado. No momento, ambos têm suporte de navegador bastante ruim (Chrome e WebKit nightlies), mas será uma combinação interessante no futuro. Por exemplo, você pode usar a sandbox para comentar em um artigo com este código:<iframe sandbox seamless srcdoc="<p>This is a user's comment! It can't execute script! Hooray for safety!</p>"></iframe>