eval() gebruiken in iframes in de sandbox

Het extensiesysteem van Chrome dwingt een vrij strikt standaard Content Security Policy (CSP) af. De beleidsbeperkingen zijn eenvoudig: het script moet out-of-line worden verplaatst naar afzonderlijke JavaScript-bestanden, inline gebeurtenishandlers moeten worden geconverteerd om addEventListener te gebruiken en eval() staat uit.

We erkennen echter dat verschillende bibliotheken eval() en eval-achtige constructies zoals new Function() gebruiken voor prestatieoptimalisatie en expressiegemak. Templating-bibliotheken zijn vooral gevoelig voor deze implementatiestijl. Hoewel sommige frameworks (zoals Angular.js) CSP standaard ondersteunen, zijn veel populaire frameworks nog niet geüpdatet naar een mechanisme dat compatibel is met de eval-loze wereld van extensies. Het verwijderen van de ondersteuning voor die functionaliteit is daarom problematischer dan verwacht voor ontwikkelaars.

In dit document wordt sandboxing geïntroduceerd als een veilig mechanisme om deze bibliotheken in je projecten op te nemen zonder de beveiliging in gevaar te brengen.

Waarom een sandbox?

eval is gevaarlijk in een extensie, omdat de code die wordt uitgevoerd toegang heeft tot alles in de omgeving met hoge rechten van de extensie. Er zijn een heleboel krachtige browser.*-API's beschikbaar die een grote impact kunnen hebben op de beveiliging en privacy van een gebruiker. Ongeoorloofde gegevensoverdracht is nog het minste van onze zorgen. De aangeboden oplossing is een sandbox waarin eval code kan uitvoeren zonder toegang tot de gegevens van de extensie of de waardevolle API's van de extensie. Geen gegevens, geen API's, geen probleem.

We doen dit door specifieke HTML-bestanden in het extensiepakket te vermelden als sandbox. Als een sandboxpagina wordt geladen, wordt deze verplaatst naar een unieke oorsprong en wordt de toegang tot browser.*-API's geweigerd. Als we deze sandbox-pagina via een iframe in onze extensie laden, kunnen we er berichten aan doorgeven, de pagina op een bepaalde manier op die berichten laten reageren en wachten tot de pagina ons een resultaat teruggeeft. Dit eenvoudige berichtmechanisme geeft ons alles wat we nodig hebben om code op basis van eval veilig op te nemen in de workflow van onze extensie.

Een sandbox maken en gebruiken

Als je meteen aan de slag wilt met de code, download je de voorbeeldextensie voor sandboxing en ga je aan de slag. Dit is een werkend voorbeeld van een kleine berichten-API die is gebouwd op de Handlebars-templatelibrary. Hiermee heb je alles wat je nodig hebt om aan de slag te gaan. Voor degenen die wat meer uitleg willen, lopen we hier samen door het voorbeeld.

Bestanden in manifest vermelden

Elk bestand dat in een sandbox moet worden uitgevoerd, moet worden vermeld in het extensiemanifest door een property sandbox toe te voegen. Dit is een cruciale stap die je makkelijk kunt vergeten. Controleer dus goed of je sandboxbestand in het manifest staat. In dit voorbeeld plaatsen we het bestand met de slimme naam 'sandbox.html' in een sandbox. De manifestvermelding ziet er zo uit:

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

Het bestand in de sandbox laden

Als we iets interessants willen doen met het bestand in de sandbox, moeten we het laden in een context waarin het kan worden geadresseerd door de code van de extensie. Hier is sandbox.html geladen in een extensiepagina met een iframe. Het JavaScript-bestand van de pagina bevat code die een bericht naar de sandbox stuurt wanneer er op de browseractie wordt geklikt. Dit gebeurt door de iframe op de pagina te zoeken en postMessage() aan te roepen op de contentWindow. Het bericht is een object met 3 eigenschappen: context, templateName en command. We gaan zo dieper in op context en command.

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

Iets gevaarlijks doen

Als sandbox.html wordt geladen, wordt de Handlebars-bibliotheek geladen en wordt een inline template gemaakt en gecompileerd op de manier die Handlebars voorstelt:

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>

Dit mislukt niet! Hoewel Handlebars.compile uiteindelijk new Function gebruikt, werkt alles precies zoals verwacht en krijgen we een gecompileerde template in templates['hello'].

Het resultaat terugsturen

We maken deze template beschikbaar voor gebruik door een berichtlistener in te stellen die opdrachten van de extensiepagina accepteert. We gebruiken de doorgegeven command om te bepalen wat er moet gebeuren (je kunt je voorstellen dat je meer doet dan alleen renderen, misschien templates maken? Misschien beheer je ze op een bepaalde manier?) en wordt de context rechtstreeks doorgegeven aan de template voor weergave. De gerenderde HTML wordt teruggestuurd naar de extensiepagina, zodat de extensie er later iets nuttigs mee kan doen:

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

Terug op de extensiepagina krijgen we dit bericht en doen we iets interessants met de html gegevens die we hebben gekregen. In dit geval herhalen we de invoer via een melding, maar het is ook mogelijk om deze HTML veilig te gebruiken als onderdeel van de UI van de extensie. Invoegen via innerHTML vormt geen aanzienlijk beveiligingsrisico, omdat we de content vertrouwen die is gerenderd in de sandbox.

Dit mechanisme maakt het maken van templates eenvoudig, maar is natuurlijk niet beperkt tot templates. Code die niet meteen werkt onder een strikt Content Security Policy, kan in een sandbox worden geplaatst. Het is vaak nuttig om componenten van je extensies die wel correct zouden worden uitgevoerd, in een sandbox te plaatsen om elk deel van je programma te beperken tot de kleinste set rechten die nodig is om het correct uit te voeren. De presentatie Writing Secure Web Apps and Chrome Extensions van Google I/O 2012 bevat een aantal goede voorbeelden van deze techniek in actie en is de moeite waard om te bekijken (56 minuten).