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 Richtlinieneinschränkungen sind einfach: Das Script muss außerhalb der Zeile in separate JavaScript-Dateien verschoben werden, Inline-Ereignishandler müssen für die Verwendung von addEventListener konvertiert werden und eval() ist deaktiviert.

Wir sind uns jedoch bewusst, dass in einer Vielzahl von Bibliotheken eval()- und eval-ähnliche Konstrukte wie new Function() zur Leistungsoptimierung und zur einfachen Ausdrucksweise verwendet werden. Vorlagenbibliotheken sind besonders anfällig für diese Art der Implementierung. Einige Frameworks (z. B. Angular.js) unterstützen CSP standardmäßig, viele beliebte Frameworks wurden jedoch noch nicht auf einen Mechanismus umgestellt, der mit der eval-freien Welt von Erweiterungen kompatibel ist. Die Einstellung der Unterstützung dieser Funktion hat sich daher für Entwickler als problematischer als erwartet erwiesen.

In diesem Dokument wird Sandboxing als sicherer Mechanismus vorgestellt, mit dem Sie diese Bibliotheken in Ihre Projekte einbinden können, ohne die Sicherheit zu gefährden.

Warum eine Sandbox?

eval ist in einer Erweiterung gefährlich, da der ausgeführte Code Zugriff auf alles in der Umgebung mit hohen Berechtigungen der Erweiterung hat. Es gibt eine Vielzahl leistungsstarker chrome.* APIs, die sich erheblich auf die Sicherheit und den Datenschutz von Nutzern auswirken können. Die einfache Datenextraktion ist dabei das geringste Problem. Die angebotene Lösung ist eine Sandbox, in der eval Code ausführen kann, ohne auf die Daten oder die wertvollen APIs der Erweiterung zuzugreifen. Keine Daten, keine APIs, kein Problem.

Dazu werden bestimmte HTML-Dateien im Erweiterungspaket als Sandbox-Dateien aufgeführt. Wenn eine Seite mit Sandbox geladen wird, wird sie an einen eindeutigen Ursprung verschoben und der Zugriff auf chrome.* APIs wird ihr verweigert. Wenn wir diese Sandbox-Seite über eine iframe in unsere Erweiterung laden, können wir ihr Nachrichten übergeben, 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 brauchen, um eval-basierten Code sicher in den Workflow unserer Erweiterung einzubinden.

Sandbox erstellen und verwenden

Wenn Sie direkt mit dem Code beginnen möchten, laden Sie die Beispielerweiterung für die Sandbox herunter. Es handelt sich dabei um ein funktionierendes Beispiel für eine kleine Messaging-API, die auf der Handlebars-Template-Bibliothek basiert. Sie sollten damit alles haben, was Sie für den Einstieg benötigen. Für diejenigen, die noch etwas mehr Erklärung benötigen, gehen wir das Beispiel hier gemeinsam durch.

Dateien im Manifest auflisten

Jede Datei, die in einer Sandbox ausgeführt werden soll, muss im Manifest der Erweiterung aufgeführt sein. Dazu fügen Sie eine sandbox-Eigenschaft hinzu. Dieser Schritt ist wichtig und leicht zu vergessen. Prüfen Sie daher noch einmal, ob Ihre Datei in der Sandbox im Manifest aufgeführt ist. In diesem Beispiel wird die Datei „sandbox.html“ in einer Sandbox ausgeführt. Der Manifesteintrag sieht so aus:

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

Datei aus der Sandbox laden

Damit wir etwas Interessantes mit der Datei in der Sandbox tun können, müssen wir sie in einem Kontext laden, in dem sie vom Code der Erweiterung angesprochen werden kann. Hier wurde „sandbox.html“ über eine iframe auf eine Erweiterungsseite geladen. Die JavaScript-Datei der Seite enthält Code, der jedes Mal, wenn auf die Browseraktion geklickt wird, eine Nachricht an die Sandbox sendet. Dazu wird die iframe auf der Seite gefunden und postMessage() über ihre contentWindow aufgerufen. Die Nachricht ist ein Objekt mit drei Eigenschaften: context, templateName und command. context und command werden wir gleich genauer betrachten.

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

Gefährliche Handlungen

Wenn sandbox.html geladen wird, wird auch die Handlebars-Bibliothek geladen. Anschließend wird eine Inline-Vorlage erstellt und kompiliert, wie von Handlebars vorgeschlagen:

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 funktioniert immer! Obwohl Handlebars.compile 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 Nachrichtenempfänger einrichten, der Befehle von der Erweiterungsseite akzeptiert. Anhand der übergebenen command wird bestimmt, was getan werden soll. Sie könnten sich vorstellen, mehr als nur zu rendern, z. B. Vorlagen zu erstellen. Vielleicht können Sie sie auf irgendeine Weise verwalten?) und die context wird direkt zum Rendern an die Vorlage übergeben. Der gerenderte HTML-Code wird an die Erweiterungsseite zurückgegeben, damit die Erweiterung damit später etwas Nützliches tun 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>

Auf der Seite der Erweiterung erhalten wir diese Nachricht und können etwas Interessantes mit den übergebenen html-Daten tun. In diesem Fall geben wir den Text einfach über eine Benachrichtigung aus. Es ist aber durchaus möglich, diesen HTML-Code sicher als Teil der Benutzeroberfläche der Erweiterung zu verwenden. Das Einfügen über innerHTML stellt kein erhebliches Sicherheitsrisiko dar, da wir den Inhalt vertrauen, der in der Sandbox gerendert wurde.

Dieser Mechanismus vereinfacht die Erstellung von Vorlagen, ist aber natürlich nicht darauf beschränkt. Jeder Code, der unter einer strengen Content Security Policy nicht standardmäßig funktioniert, kann in einer Sandbox ausgeführt werden. Tatsächlich ist es oft sinnvoll, Komponenten Ihrer Erweiterungen, die richtig ausgeführt werden würden, in einer Sandbox auszuführen, um jedes Teil Ihres Programms auf die kleinste Anzahl 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 (Sichere Webanwendungen und Chrome-Erweiterungen entwickeln) von der Google I/O 2012 enthält einige gute Beispiele für diese Technik in der Praxis und ist 56 Minuten Ihrer Zeit wert.