O sistema de extensões do Chrome aplica uma Política de Segurança de Conteúdo (CSP) padrão bastante rígida.
As restrições da política são simples: o script precisa ser movido off-line para 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 construções semelhantes a eval()
e eval
, como
new Function()
, para otimizar o desempenho 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 a CSP de forma
imediata, muitos frameworks conhecidos ainda não foram atualizados para um mecanismo compatível com
o mundo sem eval
das extensões. A remoção do suporte a essa funcionalidade provou ser mais
problemática do que o esperado para os desenvolvedores.
Este documento apresenta o sandbox como um mecanismo seguro para incluir essas bibliotecas nos seus projetos sem comprometer a segurança.
Por que usar o sandbox?
O eval
é perigoso em uma extensão porque o código que ele executa tem acesso a tudo no
ambiente de permissão alta da extensão. Há uma série de APIs chrome.*
poderosas disponíveis que podem
afetar gravemente a segurança e a privacidade de um usuário. A extração simples 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 da extensão. Sem dados, sem APIs, sem problemas.
Para isso, listamos arquivos HTML específicos no pacote de extensão como sendo sandboxed.
Sempre que uma página isolada em sandbox for carregada, ela será movida para uma origem exclusiva e o acesso às APIs chrome.*
será negado. Se carregarmos essa página isolada em nossa extensão usando um iframe
, poderemos
transmitir mensagens para ela, permitir que ela aja de alguma forma com essas mensagens e esperar que ela nos transmita um
resultado. Esse mecanismo de mensagens simples oferece tudo o que precisamos para incluir com segurança o código orientado por eval
no fluxo de trabalho da extensão.
Criar e usar um sandbox
Se você quiser mergulhar direto no código, pegue a extensão de exemplo de sandbox e comece. É um exemplo funcional de uma API de mensagens pequena criada com base na biblioteca de modelos Handlebars, que oferece tudo o que você precisa para começar. Para quem quiser mais explicações, vamos analisar esse exemplo aqui.
Listar arquivos no manifesto
Cada arquivo que precisa ser executado em um sandbox precisa ser listado no manifesto da extensão adicionando uma
propriedade sandbox
. Essa é uma etapa importante e fácil de esquecer. Verifique se
o arquivo em sandbox está listado no manifesto. Neste exemplo, estamos usando o sandbox do arquivo
chamado "sandbox.html". A entrada do manifesto é assim:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Carregar o arquivo em sandbox
Para fazer algo interessante com o arquivo em modo de proteção, precisamos fazer o carregamento em um contexto em que ele possa ser tratado 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, 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 abordar 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 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. Embora 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 precisa 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 para o 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>
Na página da extensão, vamos receber essa mensagem e fazer algo interessante com os dados html
que recebemos. Nesse caso, vamos apenas fazer o eco por meio de uma notificação, mas
é totalmente possível usar esse HTML com segurança como parte da interface da extensão. Inserir o conteúdo por meio de
innerHTML
não representa um risco de segurança significativo, porque confiamos no conteúdo renderizado
no sandbox.
Esse mecanismo simplifica a criação de modelos, mas, claro, não se limita a isso. Qualquer código que não funcione de forma nativa em uma política rígida de Content Security Policy pode ser colocado em sandbox. Na verdade, muitas vezes é útil colocar em sandbox componentes das suas extensões que seriam executados corretamente para restringir cada parte do programa ao menor conjunto de privilégios necessário para que ele seja executado corretamente. A apresentação Como criar apps da Web e extensões do Chrome seguros do Google I/O 2012 mostra alguns bons exemplos dessa técnica em ação e vale 56 minutos do seu tempo.