Gebruik eval() in iframes in een sandbox

Het extensiesysteem van Chrome dwingt een vrij strikt standaard Content Security Policy (CSP) af. De beleidsbeperkingen zijn duidelijk: het script moet buiten de regels worden verplaatst naar afzonderlijke JavaScript-bestanden, inline gebeurtenishandlers moeten worden geconverteerd om addEventListener te gebruiken, en eval() is uitgeschakeld.

We erkennen echter dat een verscheidenheid aan bibliotheken eval() en eval -achtige constructies zoals new Function() gebruiken voor prestatie-optimalisatie en expressiegemak. Sjabloonbibliotheken zijn bijzonder gevoelig voor deze implementatiestijl. Hoewel sommige (zoals Angular.js ) CSP kant-en-klaar ondersteunen, zijn veel populaire raamwerken nog niet bijgewerkt naar een mechanisme dat compatibel is met de eval -less-wereld van extensies. Het verwijderen van de ondersteuning voor die functionaliteit is voor ontwikkelaars daarom problematischer gebleken dan verwacht .

Dit document introduceert sandboxing als een veilig mechanisme om deze bibliotheken in uw projecten op te nemen zonder concessies te doen aan de beveiliging.

Waarom sandboxen?

eval is gevaarlijk binnen een extensie omdat de code die deze uitvoert toegang heeft tot alles in de omgeving met hoge machtigingen van de extensie. Er zijn een hele reeks krachtige chrome.* API's beschikbaar die ernstige gevolgen kunnen hebben voor de veiligheid en privacy van een gebruiker; eenvoudige data-exfiltratie is 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 hoogwaardige API's van de extensie. Geen data, geen API’s, geen probleem.

We bereiken dit door specifieke HTML-bestanden in het extensiepakket te vermelden als sandbox. Telkens wanneer een pagina in een sandbox wordt geladen, wordt deze verplaatst naar een unieke oorsprong en wordt de toegang tot chrome.* API's geweigerd. Als we deze in een sandbox geplaatste pagina via een iframe in onze extensie laden, kunnen we er berichten aan doorgeven, hem op de een of andere manier op die berichten laten reageren en wachten tot hij ons een resultaat terugstuurt. Dit eenvoudige berichtenmechanisme biedt ons alles wat we nodig hebben om op veilige wijze eval code op te nemen in de workflow van onze extensie.

Maak en gebruik een sandbox

Als je direct in de code wilt duiken, pak dan de sandboxing-voorbeeldextensie en start . Het is een werkend voorbeeld van een kleine berichten-API die bovenop de Handlebars- sjablonenbibliotheek is gebouwd en die je alles zou moeten geven wat je nodig hebt om aan de slag te gaan. Voor degenen onder u die wat meer uitleg willen, laten we dat voorbeeld hier samen doornemen.

Lijst bestanden in manifest

Elk bestand dat in een sandbox moet worden uitgevoerd, moet in het extensiemanifest worden vermeld door een sandbox eigenschap toe te voegen. Dit is een cruciale stap en u vergeet deze gemakkelijk. Controleer dus nogmaals of uw sandbox-bestand in het manifest wordt vermeld. In dit voorbeeld sandboxen we het bestand met de slimme naam "sandbox.html". De manifestinvoer ziet er als volgt uit:

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

Laad het sandboxbestand

Om iets interessants te doen met het sandbox-bestand, moeten we het in een context laden waarin het kan worden aangepakt door de code van de extensie. Hier is sandbox.html via een iframe in een extensiepagina geladen. Het JavaScript-bestand van de pagina bevat code die een bericht naar de sandbox stuurt wanneer op de browseractie wordt geklikt door het iframe op de pagina te vinden en postMessage() aan te roepen op de contentWindow ervan. Het bericht is een object met drie eigenschappen: context , templateName en command . We duiken zo meteen in de context en command .

service-werker.js:

chrome.action.onClicked.addListener(() => {
  chrome.tabs.create({
    url: 'mainpage.html'
  });
  console.log('Opened a tab with a sandboxed page!');
});

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

Doe iets gevaarlijks

Wanneer sandbox.html wordt geladen, wordt de Handlebars-bibliotheek geladen en wordt een inline-sjabloon gemaakt en gecompileerd op de manier zoals Handlebars suggereert:

extensie-pagina.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, werken de dingen precies zoals verwacht, en eindigen we met een gecompileerde sjabloon in templates['hello'] .

Geef het resultaat terug

We maken deze sjabloon beschikbaar voor gebruik door een berichtlistener in te stellen die opdrachten van de extensiepagina accepteert. We gebruiken het ingevoerde command om te bepalen wat er moet gebeuren (je kunt je voorstellen dat we meer doen dan alleen maar renderen; misschien sjablonen maken? Misschien ze op de een of andere manier beheren?), en de context wordt rechtstreeks in de sjabloon doorgegeven voor weergave . De weergegeven 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 ontvangen we dit bericht en doen we iets interessants met de html gegevens die ons zijn doorgegeven. In dit geval laten we het gewoon via een melding horen, maar het is heel goed mogelijk om deze HTML veilig te gebruiken als onderdeel van de gebruikersinterface van de extensie. Het invoegen ervan via innerHTML vormt geen significant veiligheidsrisico, omdat we de inhoud vertrouwen die in de sandbox is weergegeven.

Dit mechanisme maakt het maken van sjablonen eenvoudig, maar beperkt zich uiteraard niet tot het maken van sjablonen. Elke code die niet out-of-the-box werkt onder een strikt Content Security Policy kan in een sandbox worden geplaatst; in feite is het vaak nuttig om componenten van uw extensies in een sandbox te plaatsen die correct zouden werken, om elk onderdeel van uw 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 geeft enkele goede voorbeelden van deze techniek in actie en is 56 minuten van uw tijd waard.