Utiliser eval() dans des iFrames en bac à sable

Le système d'extension de Chrome applique une Content Security Policy (CSP) par défaut assez stricte. Les restrictions de 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 reconnaissons toutefois que de nombreuses bibliothèques utilisent des constructions semblables à eval() et eval, comme 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 le CSP dès le départ, de nombreux frameworks populaires n'ont pas encore migré vers un mécanisme compatible avec le monde 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 comme un mécanisme sécurisé permettant d'inclure ces bibliothèques dans vos projets sans compromettre la sécurité.

Pourquoi utiliser un bac à sable ?

eval est dangereux dans une extension, car le code qu'il exécute a accès à tout dans l'environnement à autorisation élevée de l'extension. De nombreuses API chrome.* puissantes sont disponibles et peuvent avoir un impact important sur la sécurité et la confidentialité d'un utilisateur. L'exfiltration de données simple n'est que le moindre de nos soucis. La solution proposée est un bac à sable dans lequel eval peut exécuter du code sans avoir accès aux données de l'extension ni aux API de valeur de l'extension. Pas de données, pas d'API, pas de problème.

Pour ce faire, nous listons des fichiers HTML spécifiques dans le package d'extension comme étant placés dans un bac à sable. Chaque fois qu'une page en bac à sable est chargée, elle est déplacée vers une origine unique et l'accès aux API chrome.* lui est refusé. Si nous chargeons cette page dans un bac à sable dans notre extension via un iframe, nous pouvons lui transmettre des messages, la 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 de manière sécurisée le code eval dans le workflow de notre extension.

Créer et utiliser un bac à sable

Si vous souhaitez vous lancer directement dans le code, téléchargez l'extension d'exemple de bac à sable et commencez. Il s'agit d'un exemple fonctionnel d'une petite API de messagerie basée sur la bibliothèque de modèles Handlebars. Elle devrait vous fournir tout ce dont vous avez besoin pour vous lancer. Pour ceux d'entre vous qui souhaitent obtenir des explications plus détaillées, examinons cet exemple ensemble.

Répertorier les fichiers dans le fichier manifeste

Chaque fichier qui doit être exécuté dans un bac à sable doit être listé dans le fichier manifeste de l'extension en ajoutant une propriété sandbox. Il s'agit d'une étape essentielle, mais facile à oublier. Vérifiez donc que votre fichier de bac à sable est listé dans le fichier manifeste. Dans cet exemple, nous mettons en bac à sable le fichier astucieusement nommé "sandbox.html". L'entrée du fichier manifeste se présente comme suit:

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

Charger le fichier dans un bac à sable

Pour effectuer des opérations intéressantes avec le fichier en bac à sable, nous devons le charger dans un contexte où il peut être adressé par le code de l'extension. Ici, sandbox.html a été chargé sur 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 le 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 examiner context et command dans un instant.

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

Faire 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 suggéré 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 ne doit pas échouer. 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'].

Transmettre le résultat

Nous allons rendre ce modèle disponible en configurant un écouteur de messages qui accepte les commandes de la page de l'extension. Nous utiliserons l'command transmise pour déterminer ce qui doit être fait (vous pouvez imaginer faire plus que simplement effectuer un rendu, peut-être créer des modèles ? (peut-être en les gérant d'une manière ou d'une autre ?), et le context sera transmis directement au modèle pour l'affichage. Le code HTML affiché sera renvoyé à la page de l'extension afin qu'elle puisse l'utiliser plus tard:

 <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 recevons ce message et effectuons une action intéressante avec les données html qui nous ont été transmises. Dans ce cas, nous allons simplement l'afficher via une notification, mais il est tout à fait possible d'utiliser ce code HTML de manière sécurisée dans l'UI de l'extension. L'insertion via innerHTML ne pose pas de risque de sécurité important, car nous faisons confiance au contenu qui a été rendu dans le bac à sable.

Ce mécanisme simplifie la création de modèles, mais ne s'y limite pas. Tout code qui ne fonctionne pas immédiatement sous une règle de sécurité du contenu stricte peut être placé dans un bac à sable. En fait, il est souvent utile de placer dans un bac à sable les composants de vos extensions qui s'exécuteraient correctement afin de limiter chaque partie de votre programme au plus petit ensemble de privilèges nécessaire pour son exécution. La présentation Écrire des applications Web sécurisées et des extensions Chrome de Google I/O 2012 donne de bons exemples de ces techniques en action et vous fera gagner 56 minutes.