Cómo usar eval() en iframes de zona de pruebas

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 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. Algunos (como Angular.js) admiten CSP de inmediato, muchos frameworks populares aún no se actualizan a un mecanismo que sea compatible extensiones' Un mundo sin eval Por lo tanto, se ha demostrado que eliminar la compatibilidad con esa función es más problemático de lo esperado.

En este documento, se presenta el entorno de pruebas como un mecanismo seguro para incluir estas bibliotecas en tus proyectos sin comprometer la seguridad.

¿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 en el entorno de permisos altos 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. La extracción de datos simple es lo menos que nos preocupa. 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. Sin datos, API ni problema.

Para ello, enumeramos archivos HTML específicos dentro del paquete de extensión como zona de pruebas. Cuando se cargue una página de zona de pruebas, se moverá a un origen único y se denegará 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 pasar los mensajes, dejar que actúe sobre ellos de alguna manera y esperar a que nos envíe resultado. Este mecanismo de mensajería simple nos brinda todo lo que necesitamos para incluir de forma segura el código impulsado 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, toma la extensión de muestra de zona de pruebas y comienza. Es un ejemplo en funcionamiento de una pequeña API de mensajería compilada sobre la biblioteca de plantillas Handlebars y debería brindarte todo lo que necesitas para comenzar. Para aquellos que un poco más de explicación, analicemos ese ejemplo juntos.

Muestra una lista de archivos en el manifiesto

Cada archivo que se debe ejecutar dentro de una zona de pruebas debe aparecer en el manifiesto de la extensión. Para ello, agrega una propiedad sandbox. Este es un paso fundamental y es fácil olvidarlo, así que verifica que el archivo de zona de pruebas aparezca en el manifiesto. En este ejemplo, estamos probando el archivo en una zona de pruebas inteligente. llamada "sandbox.html". La entrada de 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 en 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 una página de extensión a través de 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, ya que encuentra 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, '*');
  });

Haz 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. Si bien Handlebars.compile termina usando new Function, las cosas funcionan exactamente como se esperaba, y obtendremos una plantilla compilada en templates['hello'].

Pasa el resultado

Esta plantilla estará disponible para su uso configurando un objeto de escucha de mensajes que acepte comandos. desde la página de la extensión. Usaremos el command que se pasó para determinar lo que se debe hacer (podrías imagina hacer más que simplemente renderizar; como crear plantillas. ¿Quizás administrarlos de alguna manera?), y el context se pasará directamente a la plantilla para su renderización. El HTML renderizado se enviará de vuelta a la página de la extensión para que la extensión pueda hacer algo útil 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>

En la página de la extensión, recibiremos este mensaje y haremos algo interesante con los datos de html que se nos pasaron. En este caso, solo lo repetiremos a través de una notificación, 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 supone un riesgo de seguridad significativo, ya que confiamos en el contenido que se renderizó en la zona de pruebas.

Este mecanismo simplifica la creación de plantillas, pero, por supuesto, no se limita a eso. Cualquiera El código que no funciona de inmediato bajo una política estricta de seguridad del contenido puede quedar en zona de pruebas. en De hecho, suele ser útil hacer una zona de pruebas de los componentes de las extensiones que podrían ejecutarse correctamente en orden restringir cada parte de tu programa al conjunto más pequeño de privilegios necesarios para que se ejecuten correctamente. La presentación Cómo escribir apps web y extensiones de Chrome seguras de Google I/O 2012 ofrece algunos buenos ejemplos de estas técnicas en acción y vale la pena dedicarle 56 minutos.