Utiliser eval() dans des iFrames en bac à sable

Le système d'extensions de Chrome applique une Content Security Policy (CSP) par défaut assez stricte. Les restrictions des règles sont simples: le script doit être déplacé hors ligne dans des fichiers JavaScript distincts, les gestionnaires d'événements intégrés doivent être convertis pour utiliser addEventListener, et eval() est désactivé.

Nous sommes toutefois conscients que diverses bibliothèques utilisent des constructions de type eval() et eval, telles que new Function(), pour optimiser les performances et faciliter l'expression. Les bibliothèques de modèles sont particulièrement sujettes à ce style d'implémentation. Bien que certains (comme Angular.js) soient compatibles avec CSP par défaut, de nombreux frameworks courants ne disposent pas encore d'un mécanisme compatible avec l'environnement sans eval des extensions. La suppression de la prise en charge de cette fonctionnalité s'est donc révélée plus problématique que prévu pour les développeurs.

Ce document présente le bac à sable, un mécanisme sécurisé permettant d'inclure ces bibliothèques dans vos projets sans compromettre la sécurité.

Pourquoi utiliser le bac à sable ?

eval est dangereux dans une extension, car le code qu'elle exécute a accès à tous les éléments de l'environnement de haute autorisation de l'extension. Une multitude d'API chrome.* performantes sont disponibles et peuvent avoir un impact important sur la sécurité et la confidentialité des utilisateurs. L'exfiltration simple des données ne nous pose pas de problème. La solution proposée est un bac à sable dans lequel eval peut exécuter du code sans accéder aux données de l'extension ni aux API à forte valeur ajoutée de celle-ci. Pas de données, pas d'API, pas de problème.

Pour ce faire, nous indiquons que des fichiers HTML spécifiques du package de l'extension sont en bac à sable. Chaque fois qu'une page en bac à sable est chargée, elle est déplacée vers une origine unique et se voit refuser l'accès aux API chrome.*. Si nous chargeons cette page en bac à sable dans notre extension via un iframe, nous pouvons lui transmettre des messages, le laisser agir sur ces messages d'une manière ou d'une autre et attendre qu'elle nous renvoie un résultat. Ce mécanisme de messagerie simple nous fournit tout ce dont nous avons besoin pour inclure en toute sécurité du code basé sur eval dans le workflow de notre extension.

Créer et utiliser un bac à sable

Si vous souhaitez vous plonger dans le code, téléchargez l'exemple d'extension de bac à sable et décollez. Il s'agit d'un exemple concret d'API de messagerie miniature construite sur la bibliothèque de modèles Handlebars. Elle devrait vous fournir tout ce dont vous avez besoin pour commencer. Pour ceux d'entre vous qui souhaitent obtenir plus d'explications, examinons cet exemple ensemble.

Lister les fichiers dans le fichier manifeste

Chaque fichier devant être exécuté dans un bac à sable doit être répertorié dans le fichier manifeste de l'extension en ajoutant une propriété sandbox. Il s'agit d'une étape essentielle et on peut facilement l'oublier. Vérifiez donc que le fichier en bac à sable figure bien dans le fichier manifeste. Dans cet exemple, nous exécutons un bac à sable pour le fichier astucieux nommé "sandbox.html". L'entrée du fichier manifeste se présente comme suit:

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

Charger le fichier en bac à sable

Pour effectuer une opération intéressante avec le fichier en bac à sable, nous devons le charger dans un contexte où le code de l'extension peut le résoudre. Ici, sandbox.html a été chargé dans une page d'extension via un iframe. Le fichier JavaScript de la page contient du code qui envoie un message dans le bac à sable chaque fois que l'utilisateur clique sur l'action du navigateur en recherchant iframe sur la page et en appelant postMessage() sur son contentWindow. Le message est un objet contenant trois propriétés: context, templateName et command. Nous allons maintenant nous intéresser à context et command.

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, '*');
  });

Fait quelque chose de dangereux

Lorsque sandbox.html est chargé, il charge la bibliothèque Handlebars, puis crée et compile un modèle intégré comme indiqué par 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>

Cela n'échoue pas ! Même si Handlebars.compile finit par utiliser new Function, tout fonctionne exactement comme prévu et nous obtenons un modèle compilé dans templates['hello'].

Renvoyer le résultat

Nous allons rendre ce modèle utilisable en configurant un écouteur de message qui accepte les commandes de la page de l'extension. Nous utiliserons l'élément command transmis pour déterminer l'action à effectuer (vous pourriez imaginer faire plus que simplement afficher ; par exemple créer des modèles ? Peut-être les gérer d'une manière ou d'une autre ?), et context sera directement transmis au modèle pour le rendu. Le rendu HTML sera renvoyé à la page de l'extension afin que l'extension puisse en faire quelque chose d'utile par la suite:

 <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 retour sur la page de l'extension, nous allons recevoir ce message et effectuer une action intéressante avec les données html qui nous ont été transmises. Dans ce cas, nous nous contenterons de renvoyer ce code via une notification, mais il est tout à fait possible d'utiliser ce code HTML de manière sécurisée dans l'interface utilisateur de l'extension. L'insérer via innerHTML ne présente pas de risque de sécurité important, car nous faisons confiance au contenu affiché dans le bac à sable.

Ce mécanisme facilite la création de modèles, mais il ne se limite évidemment pas à la création de modèles. Tout code qui ne fonctionne pas directement avec une Content Security Policy stricte peut être exécuté en bac à sable. En fait, il est souvent utile de créer un bac à sable pour les composants de vos extensions qui s'exécutent correctement afin de limiter chaque élément de votre programme au plus petit ensemble de droits nécessaire à son exécution correcte. La présentation Écrire des applications Web sécurisées et des extensions Chrome de la conférence Google I/O 2012 présente de bons exemples d'utilisation concrète de ces techniques. Elle vaut 56 minutes de votre temps.