eval() in iFrames verwenden, die in einer Sandbox ausgeführt werden

Das Erweiterungssystem von Chrome erzwingt eine ziemlich strenge Standard-Content Security Policy (CSP). Die Richtlinienbeschränkungen sind einfach: Das Script muss aus dem Inline-Code in separate JavaScript-Dateien verschoben werden, Inline-Event-Handler müssen so konvertiert werden, dass sie addEventListener verwenden, und eval() ist deaktiviert.

Wir wissen jedoch, dass in einer Vielzahl von Bibliotheken Konstrukte wie eval() und eval verwendet werden, z. B. new Function(), um die Leistung zu optimieren und die Ausdrucksweise zu vereinfachen. Vorlagenbibliotheken sind besonders anfällig für diese Art der Implementierung. Einige Frameworks wie Angular.js unterstützen CSP sofort, viele beliebte Frameworks wurden jedoch noch nicht auf einen Mechanismus aktualisiert, der mit der eval-losen Welt von Erweiterungen kompatibel ist. Das Entfernen der Unterstützung für diese Funktion hat sich daher für Entwickler als problematischer als erwartet erwiesen.

In diesem Dokument wird Sandboxing als sicherer Mechanismus zum Einbinden dieser Bibliotheken in Ihre Projekte vorgestellt, ohne die Sicherheit zu beeinträchtigen.

Warum Sandbox?

eval ist in einer Erweiterung gefährlich, da der ausgeführte Code Zugriff auf alle Elemente in der Umgebung mit hohen Berechtigungen der Erweiterung hat. Es sind viele leistungsstarke browser.*-APIs verfügbar, die sich erheblich auf die Sicherheit und den Datenschutz eines Nutzers auswirken können. Die einfache Daten-Exfiltration ist dabei noch das geringste Problem. Die angebotene Lösung ist eine Sandbox, in der eval Code ausführen kann, ohne Zugriff auf die Daten oder die APIs mit hohem Wert der Erweiterung zu haben. Keine Daten, keine APIs, kein Problem.

Dazu werden bestimmte HTML-Dateien im Erweiterungspaket als Sandbox-Dateien aufgeführt. Jedes Mal, wenn eine Seite in der Sandbox geladen wird, wird sie zu einem eindeutigen Ursprung verschoben und der Zugriff auf browser.*-APIs wird verweigert. Wenn wir diese Sandbox-Seite über ein iframe in unsere Erweiterung laden, können wir ihr Nachrichten senden, sie auf diese Nachrichten reagieren lassen und darauf warten, dass sie uns ein Ergebnis zurückgibt. Dieser einfache Messaging-Mechanismus bietet uns alles, was wir benötigen, um eval-gesteuerten Code sicher in den Workflow unserer Erweiterung einzubinden.

Sandbox erstellen und verwenden

Wenn Sie direkt mit dem Code loslegen möchten, laden Sie die Beispielerweiterung für das Sandboxing herunter. Sie ist ein funktionierendes Beispiel für eine kleine Messaging-API, die auf der Handlebars-Vorlagenbibliothek basiert und Ihnen alles bietet, was Sie für den Einstieg benötigen. Wenn Sie eine genauere Erklärung wünschen, sehen wir uns das Beispiel hier gemeinsam an.

Dateien im Manifest auflisten

Jede Datei, die in einer Sandbox ausgeführt werden soll, muss im Erweiterungsmanifest durch Hinzufügen einer sandbox-Eigenschaft aufgeführt werden. Dieser Schritt ist wichtig und wird leicht vergessen. Prüfen Sie daher noch einmal, ob Ihre Sandbox-Datei im Manifest aufgeführt ist. In diesem Beispiel wird die Datei mit dem passenden Namen „sandbox.html“ in einer Sandbox ausgeführt. Der Manifesteintrag sieht so aus:

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

Sandboxed-Datei laden

Damit wir mit der Sandbox-Datei etwas Interessantes anstellen können, müssen wir sie in einem Kontext laden, in dem sie vom Code der Erweiterung angesprochen werden kann. Hier wurde „sandbox.html“ mit einem iframe auf eine Erweiterungsseite geladen. Die JavaScript-Datei der Seite enthält Code, der eine Nachricht in die Sandbox sendet, wenn auf die Browseraktion geklickt wird. Dazu wird das iframe auf der Seite gesucht und postMessage() für das contentWindow aufgerufen. Die Nachricht ist ein Objekt mit drei Attributen: context, templateName und command. Wir werden uns context und command gleich genauer ansehen.

service-worker.js:

browser.action.onClicked.addListener(() => {
  browser.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, '*');
  });

Gefährliche Dinge tun

Wenn sandbox.html geladen wird, wird die Handlebars-Bibliothek geladen und ein Inline-Template wird auf die von Handlebars empfohlene Weise erstellt und kompiliert:

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>

Das schlägt nie fehl. Obwohl Handlebars.compile letztendlich new Function verwendet, funktioniert alles wie erwartet und wir erhalten eine kompilierte Vorlage in templates['hello'].

Ergebnis zurückgeben

Wir stellen diese Vorlage zur Verfügung, indem wir einen Nachrichten-Listener einrichten, der Befehle von der Erweiterungsseite akzeptiert. Wir verwenden den übergebenen command, um zu bestimmen, was zu tun ist. Es ist denkbar, dass wir mehr als nur rendern, z. B. Vorlagen erstellen. context wird direkt an die Vorlage übergeben, um gerendert zu werden. Der gerenderte HTML-Code wird an die Erweiterungsseite zurückgegeben, damit die Erweiterung später etwas damit anfangen kann:

 <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>

Zurück auf der Erweiterungsseite wird diese Meldung angezeigt und wir können mit den übergebenen html-Daten etwas Interessantes anstellen. In diesem Fall geben wir sie einfach über eine Benachrichtigung aus. Es ist jedoch durchaus möglich, dieses HTML sicher als Teil der Benutzeroberfläche der Erweiterung zu verwenden. Das Einfügen über innerHTML birgt kein erhebliches Sicherheitsrisiko, da wir den Inhalt, der in der Sandbox gerendert wurde, als vertrauenswürdig einstufen.

Dieser Mechanismus macht die Verwendung von Vorlagen einfach, ist aber natürlich nicht darauf beschränkt. Jeder Code, der unter einer strengen Content Security Policy nicht sofort funktioniert, kann in einer Sandbox ausgeführt werden. Tatsächlich ist es oft sinnvoll, Komponenten Ihrer Erweiterungen, die korrekt ausgeführt werden, in einer Sandbox auszuführen, um jede Komponente Ihres Programms auf die kleinste Gruppe von Berechtigungen zu beschränken, die für die ordnungsgemäße Ausführung erforderlich sind. Die Präsentation Writing Secure Web Apps and Chrome Extensions von Google I/O 2012 enthält einige gute Beispiele für diese Techniken in der Praxis und ist 56 Minuten Ihrer Zeit wert.