El sistema de extensiones de Chrome aplica una Política de Seguridad del Contenido (CSP) predeterminada bastante estricta.
Las restricciones de la política son sencillas: la secuencia de comandos debe moverse fuera de la línea a archivos JavaScript separados, los controladores de eventos intercalados deben convertirse para usar addEventListener y eval() está inhabilitado.
Sin embargo, reconocemos que una variedad de bibliotecas usan construcciones similares a eval() y eval, como new Function(), para la optimización del rendimiento y la facilidad de expresión. Las bibliotecas de plantillas son especialmente propensas a este estilo de implementación. Si bien algunos (como Angular.js) admiten la CSP de inmediato, muchos frameworks populares aún no se actualizaron a un mecanismo compatible con el mundo sin eval de las extensiones. Por lo tanto, quitar la compatibilidad con esa funcionalidad resultó más problemático de lo esperado para los desarrolladores.
En este documento, se presenta el aislamiento como un mecanismo seguro para incluir estas bibliotecas en tus proyectos sin comprometer la seguridad.
¿Por qué usar la zona de pruebas?
eval es peligroso dentro de una extensión porque el código que ejecuta tiene acceso a todo en el entorno de alta autorización de la extensión. Hay una gran cantidad de APIs de chrome.* potentes disponibles que podrían afectar gravemente la seguridad y la privacidad de un usuario. El robo de datos simple es la menor de nuestras preocupaciones.
La solución que se ofrece es una zona de pruebas en la que eval puede ejecutar código sin acceso a los datos ni a las APIs de alto valor de la extensión. No hay datos, no hay APIs, no hay problema.
Para lograrlo, incluimos archivos HTML específicos dentro del paquete de extensión como archivos de zona de pruebas.
Cada vez que se cargue una página en zona de pruebas, se trasladará a un origen único y se le denegará el acceso a las APIs de chrome.*. Si cargamos esta página en zona de pruebas en nuestra extensión a través de un iframe, podemos pasarle mensajes, dejar que actúe sobre esos mensajes de alguna manera y esperar a que nos devuelva un resultado. Este sencillo mecanismo de mensajería nos brinda todo lo que necesitamos para incluir de forma segura código controlado por eval en el flujo de trabajo de nuestra extensión.
Crea y usa una zona de pruebas
Si deseas sumergirte directamente en el código, obtén la extensión de ejemplo de zona de pruebas y comienza a usarla. Es un ejemplo funcional de una pequeña API de mensajería creada sobre la biblioteca de plantillas Handlebars, y debería proporcionarte todo lo que necesitas para comenzar. Para quienes deseen una explicación más detallada, analicemos juntos este ejemplo.
Enumera los archivos del manifiesto
Cada archivo que se debe ejecutar dentro de una zona de pruebas debe aparecer en el manifiesto de la extensión agregando una propiedad sandbox. Este es un paso fundamental y es fácil olvidarlo, así que verifica que tu archivo en zona de pruebas aparezca en el manifiesto. En este ejemplo, creamos un espacio aislado para el archivo llamado "sandbox.html". La entrada del manifiesto se ve de la siguiente manera:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Carga el archivo en zona de pruebas
Para hacer algo interesante con el archivo en zona de pruebas, debemos cargarlo en un contexto en el que el código de la extensión pueda hacer referencia a él. Aquí, sandbox.html se cargó en una página de extensión con un iframe. El archivo JavaScript de la página contiene código que envía un mensaje a la zona de pruebas cada vez que se hace clic en la acción del navegador. Para ello, busca el iframe en la página y llama a postMessage() en su contentWindow. El mensaje es un objeto que contiene tres propiedades: context, templateName y command. Analizaremos context y command en un momento.
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, '*');
});
Hacer algo peligroso
Cuando se carga sandbox.html, se carga la biblioteca de Handlebars y se crea y compila una plantilla intercalada de la manera que sugiere 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>
Esto no falla. Aunque Handlebars.compile termina usando new Function, todo funciona exactamente como se espera y terminamos con una plantilla compilada en templates['hello'].
Cómo devolver el resultado
Para que esta plantilla esté disponible, configuraremos un objeto de escucha de mensajes que acepte comandos de la página de extensión. Usaremos el command que se pasó para determinar qué se debe hacer (podrías imaginar hacer algo más que simplemente renderizar, tal vez crear plantillas). Quizás administrarlos de alguna manera), y el context se pasará directamente a la plantilla para la renderización. El HTML renderizado se devolverá a la página de extensión para que esta pueda hacer algo útil con él más adelante:
<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 vuelta en la página de la extensión, recibiremos este mensaje y haremos algo interesante con los datos de html que recibimos. En este caso, solo lo mostraremos a través de una notificación, pero es perfectamente posible usar este código HTML de forma segura como parte de la IU de la extensión. Insertarlo a través de innerHTML no supone un riesgo de seguridad significativo, ya que confiamos en el contenido que se renderizó en el entorno de pruebas.
Este mecanismo facilita la creación de plantillas, pero, por supuesto, no se limita a eso. Cualquier código que no funcione de inmediato con una política de seguridad del contenido estricta se puede ejecutar en una zona de pruebas. De hecho, a menudo es útil ejecutar en una zona de pruebas los componentes de tus extensiones que se ejecutarían correctamente para restringir cada parte de tu programa al conjunto más pequeño de privilegios necesarios para que se ejecute correctamente. La presentación Writing Secure Web Apps and Chrome Extensions de Google I/O 2012 ofrece algunos buenos ejemplos de estas técnicas en acción y vale la pena dedicarle 56 minutos.