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 de política são simples: o script precisa ser movido para fora da linha em arquivos
JavaScript separados, os manipuladores de eventos inline precisam ser convertidos para usar addEventListener, e eval() é
desativado.
No entanto, reconhecemos que várias bibliotecas usam eval() e construções semelhantes a eval, como
new Function(), para otimizar a performance e facilitar a expressão. As bibliotecas de modelos são especialmente propensas a esse estilo de implementação. Embora alguns (como o Angular.js) ofereçam suporte à CSP de
imediato, muitos frameworks conhecidos ainda não foram atualizados para um mecanismo compatível com
o mundo sem eval das extensões. Por isso, remover o suporte a essa funcionalidade se mostrou mais
problemático do que o esperado para os desenvolvedores.
Este documento apresenta o sandboxing como um mecanismo seguro para incluir essas bibliotecas nos seus projetos sem comprometer a segurança.
Por que usar o sandbox?
eval é perigoso em uma extensão porque o código executado tem acesso a tudo no ambiente de alta permissão da extensão. Há várias APIs chrome.* poderosas disponíveis que podem afetar gravemente a segurança e a privacidade de um usuário. A exfiltração de dados simples é a menor das nossas preocupações.
A solução oferecida é um sandbox em que o eval pode executar código sem acesso aos dados ou às APIs de alto valor da extensão. Sem dados, sem APIs, sem problemas.
Para isso, listamos arquivos HTML específicos dentro do pacote de extensão como em sandbox.
Sempre que uma página em sandbox é carregada, ela é movida para uma origem exclusiva e tem o acesso negado às APIs chrome.*. Se carregarmos essa página em sandbox na nossa extensão usando um iframe, poderemos
enviar mensagens para ela, permitir que ela processe essas mensagens de alguma forma e esperar que ela nos retorne um
resultado. Esse mecanismo de mensagens simples oferece tudo o que precisamos para incluir com segurança código
orientado por eval no fluxo de trabalho da nossa extensão.
Criar e usar uma sandbox
Se você quiser começar a programar, baixe a extensão de exemplo de sandboxing e comece. É um exemplo funcional de uma pequena API de mensagens criada com base na biblioteca de modelos Handlebars, e ela oferece tudo o que você precisa para começar. Para quem quiser mais explicações, vamos analisar esse exemplo juntos.
Listar arquivos no manifesto
Cada arquivo que precisa ser executado em uma sandbox precisa ser listado no manifesto da extensão adicionando uma propriedade sandbox. Essa é uma etapa essencial, mas é fácil de esquecer. Por isso, verifique se o arquivo isolado está listado no manifesto. Neste exemplo, estamos colocando em sandbox o arquivo
chamado "sandbox.html". A entrada do manifesto é semelhante a esta:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Carregar o arquivo em sandbox
Para fazer algo interessante com o arquivo em sandbox, precisamos carregá-lo em um contexto em que
ele possa ser abordado pelo código da extensão. Aqui, o sandbox.html foi carregado em
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. Para isso, ele encontra o iframe na página e chama postMessage() no contentWindow. A mensagem é um objeto que contém três propriedades: context, templateName e command. Vamos falar sobre context e command daqui a pouco.
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 e cria e compila um modelo
inline da maneira sugerida pelo Handlebars:
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! Mesmo que 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 deve ser feito. Você pode imaginar fazer mais do que apenas renderizar, talvez criar modelos? Talvez gerenciando-os de alguma forma?), 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>
De volta à página da extensão, vamos receber esta mensagem e fazer algo interessante com os dados html
que recebemos. Nesse caso, vamos apenas ecoar isso por uma notificação, mas é totalmente possível usar esse HTML com segurança como parte da interface da extensão. Inserir o código usando
innerHTML não representa um risco de segurança significativo, já que confiamos no conteúdo renderizado
na caixa de areia.
Esse mecanismo facilita a criação de modelos, mas não se limita a isso. Qualquer código que não funcione imediatamente em uma Política de Segurança de Conteúdo estrita pode ser colocado em sandbox. Na verdade, muitas vezes é útil colocar em sandbox componentes das extensões que funcionariam corretamente para restringir cada parte do programa ao menor conjunto de privilégios necessário para que ele seja executado corretamente. A apresentação Writing Secure Web Apps and Chrome Extensions (em inglês) do Google I/O 2012 dá bons exemplos dessas técnicas em ação e vale a pena assistir por 56 minutos.