Cómo usar eval en las extensiones de Chrome

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 se debe mover fuera de línea a archivos JavaScript separados, los controladores de eventos intercalados deben convertirse para usar addEventListener y eval() debe estar inhabilitado. Las Apps de Chrome tienen una política aún más estricta, y estamos bastante satisfechos con las propiedades de seguridad que proporcionan.

Sin embargo, reconocemos que varias bibliotecas usan construcciones similares a eval() y eval, como new Function(), para optimizar el rendimiento y facilitar la expresión. Las bibliotecas de plantillas son especialmente propensas a este estilo de implementación. Si bien algunos (como Angular.js) admiten CSP de manera predeterminada, muchos frameworks populares aún no se han actualizado a un mecanismo compatible con el mundo sin eval de las extensiones. Por lo tanto, quitar la compatibilidad con esa funcionalidad demostró ser más problemático de lo esperado para los desarrolladores.

En este documento, se presenta la zona de pruebas como un mecanismo seguro para incluir estas bibliotecas en tus proyectos sin comprometer la seguridad. Para abreviar, usaremos el término extensiones en todo el documento, pero el concepto se aplica de la misma manera a las aplicaciones.

¿Por qué usar una zona de pruebas?

eval es peligroso dentro de una extensión porque el código que ejecuta tiene acceso a todo lo que se encuentra en el entorno de alto permiso de la extensión. Hay una gran cantidad de APIs de chrome.* potentes disponibles que pueden tener un impacto grave en 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 acceder a los datos de la extensión o a las APIs de alto valor de la extensión. ¿No tienes datos, APIs? No hay problema.

Para ello, enumeramos archivos HTML específicos dentro del paquete de la extensión como zona de pruebas. Cada vez que se cargue una página de zona de pruebas, se moverá a un origen único y se le denegará el acceso a las APIs de chrome.*. Si cargamos esta página de 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 pase un resultado. Este mecanismo de mensajería simple nos brinda todo lo que necesitamos para incluir de forma segura código basado en eval en el flujo de trabajo de nuestra extensión.

Crear y usar una zona de pruebas

Si quieres profundizar en el código, toma la extensión de muestra de la zona de pruebas y comienza a usarla. Es un ejemplo en funcionamiento de una API de mensajería pequeña compilada sobre la biblioteca de plantillas de Handlebars que debería brindarte todo lo que necesitas para comenzar. Para aquellos que quieren un poco más de explicación, analicemos juntos ese ejemplo.

Muestra una lista de los archivos del manifiesto

Cada archivo que se deba ejecutar dentro de una zona de pruebas debe incluirse en el manifiesto de la extensión agregando una propiedad sandbox. Este es un paso fundamental, y es fácil olvidarse. Por lo tanto, vuelve a verificar que tu archivo de zona de pruebas aparezca en el manifiesto. En esta muestra, estamos probando el archivo llamado “sandbox.html”, de forma inteligente. La entrada del manifiesto se ve de la siguiente manera:

{
  ...,
  "sandbox": {
     "pages": ["sandbox.html"]
  },
  ...
}

Carga el archivo de zona de pruebas

Para hacer algo interesante con el archivo de zona de pruebas, debemos cargarlo en un contexto en el que el código de la extensión pueda abordarlo. Aquí, sandbox.html se cargó en la página del evento de la extensión (eventpage.html) a través de un iframe. eventpage.js 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 iframe en la página y ejecuta el método postMessage en su contentWindow. El mensaje es un objeto que contiene dos propiedades: context y command. Analizaremos ambas en un momento.

chrome.browserAction.onClicked.addListener(function() {
 var iframe = document.getElementById('theFrame');
 var message = {
   command: 'render',
   context: {thing: 'world'}
 };
 iframe.contentWindow.postMessage(message, '*');
});
Para obtener información general sobre la API de postMessage, consulta la documentación de postMessage sobre MDN . Está completa y vale la pena leerla. En particular, ten en cuenta que los datos solo se pueden pasar de un lado a otro si son serializables. Las funciones, por ejemplo, no lo son.

Hacer algo peligroso

Cuando se carga sandbox.html, carga la biblioteca de Handlebars y crea y compila una plantilla intercalada de la manera en que Handlebars sugiere lo siguiente:

<script src="handlebars-1.0.0.beta.6.js"></script>
<script id="hello-world-template" type="text/x-handlebars-template">
  <div class="entry">
    <h1>Hello, !</h1>
  </div>
</script>
<script>
  var templates = [];
  var source = document.getElementById('hello-world-template').innerHTML;
  templates['hello'] = Handlebars.compile(source);
</script>

Esto no falla. Aunque Handlebars.compile termina usando new Function, todo funciona exactamente como se espera y obtenemos una plantilla compilada en templates['hello'].

Pasa el resultado de vuelta

Pondremos esta plantilla a disposición para su uso mediante la configuración de un objeto de escucha de mensajes que acepte comandos de la página Evento. Usaremos el command que se pasó para determinar lo que se debe hacer (puedes imaginar hacer algo más que solo renderizar, tal vez creando plantillas?, ¿Tal vez los administres de alguna manera?) y context se pasará directamente a la plantilla para su renderización. El HTML renderizado se pasará a la página del evento para que la extensión pueda realizar acciones útiles más adelante:

<script>
  window.addEventListener('message', function(event) {
    var command = event.data.command;
    var name = event.data.name || 'hello';
    switch(command) {
      case 'render':
        event.source.postMessage({
          name: name,
          html: templates[name](event.data.context)
        }, event.origin);
        break;

      // case 'somethingElse':
      //   ...
    }
  });
</script>

En la página del evento, recibiremos este mensaje y haremos algo interesante con los datos de html que se nos pasaron. En este caso, lo repetiremos a través de una notificación de escritorio, pero es 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 implica un riesgo de seguridad significativo, ya que ni siquiera un compromiso completo del código de la zona de pruebas a través de un ataque inteligente no podría inyectar contenido peligroso de secuencias de comandos o complementos en el contexto de extensión de alto permiso.

Este mecanismo hace que la creación de plantillas sea sencilla, pero, por supuesto, no se limita a la creación de plantillas. Cualquier código que no funcione de inmediato en una Política de Seguridad del Contenido estricta se puede poner en una zona de pruebas. De hecho, suele ser útil poner en 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 Cómo escribir apps web seguras y extensiones de Chrome de Google I/O 2012 ofrece algunos buenos ejemplos de estas técnicas en acción y vale 56 minutos de tu tiempo.