Gebruik eval() in iframes in een sandbox

Het extensiesysteem van Chrome hanteert een vrij strikt standaard Content Security Policy (CSP) . De beleidsbeperkingen zijn eenvoudig: scripts moeten uit de code worden gehaald en in aparte JavaScript-bestanden worden geplaatst, inline eventhandlers moeten worden omgezet naar het gebruik van addEventListener en eval() is uitgeschakeld.

We erkennen echter dat diverse libraries gebruikmaken van eval() en eval constructies zoals new Function() voor prestatieoptimalisatie en een eenvoudigere expressie. Templatebibliotheken zijn met name gevoelig voor deze implementatiestijl. Hoewel sommige (zoals Angular.js ) standaard CSP ondersteunen, hebben veel populaire frameworks hun mechanisme nog niet geüpdatet naar een mechanisme dat compatibel is met de ` eval -loze wereld van extensies. Het verwijderen van ondersteuning voor deze functionaliteit is daarom problematischer gebleken dan verwacht voor ontwikkelaars.

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

Waarom een ​​sandbox?

eval binnen een extensie is gevaarlijk omdat de code die het uitvoert toegang heeft tot alles in de omgeving met hoge toegangsrechten van de extensie. Er zijn talloze krachtige chrome.* API's beschikbaar die de beveiliging en privacy van een gebruiker ernstig in gevaar kunnen brengen; eenvoudige data-exfiltratie is het minste van onze zorgen. De voorgestelde 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 bereiken dit door specifieke HTML-bestanden in het extensiepakket als gesandboxt aan te merken. Wanneer een gesandboxte pagina wordt geladen, wordt deze naar een unieke oorsprong verplaatst en wordt de toegang tot chrome.* API's geweigerd. Als we deze gesandboxte pagina via een iframe in onze extensie laden, kunnen we er berichten naar sturen, de extensie op die berichten laten reageren en wachten op een resultaat. Dit eenvoudige berichtenmechanisme biedt ons alles wat we nodig hebben om veilig eval -gestuurde code in de workflow van onze extensie op te nemen.

Maak en gebruik een sandbox.

Als je meteen aan de slag wilt met coderen, download dan de sandbox-voorbeeldextensie en ga direct aan de slag . Het is een werkend voorbeeld van een kleine berichten-API, gebouwd bovenop de Handlebars -sjabloonbibliotheek, en het zou je alles moeten geven wat je nodig hebt om te beginnen. Voor degenen die wat meer uitleg willen, zullen we het voorbeeld hier samen doorlopen.

Lijst met bestanden in manifest

Elk bestand dat in een sandbox moet worden uitgevoerd, moet in het extensiemanifest worden opgenomen door een sandbox eigenschap toe te voegen. Dit is een cruciale stap die gemakkelijk vergeten kan worden, dus controleer goed of uw bestand in de sandbox in het manifest staat. In dit voorbeeld plaatsen we het bestand met de slimme naam "sandbox.html" in een sandbox. De vermelding in het manifest ziet er als volgt uit:

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

Laad het afgeschermde bestand.

Om iets interessants te doen met het afgeschermde bestand, moeten we het laden in een context waarin het kan worden aangesproken door de code van de extensie. Hier is sandbox.html geladen in een extensiepagina met behulp van 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 het iframe op de pagina te vinden en postMessage() aan te roepen op contentWindow ervan. Het bericht is een object met drie eigenschappen: context , templateName en command . We zullen context en command zo meteen nader bekijken.

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

Doe iets gevaarlijks

Wanneer sandbox.html wordt geladen, laadt het de Handlebars-bibliotheek en maakt en compileert het een inline-sjabloon op de manier die Handlebars aanbeveelt:

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 werkt perfect! Hoewel Handlebars.compile uiteindelijk new Function gebruikt, werkt alles precies zoals verwacht en krijgen we een gecompileerde template in templates['hello'] .

Geef het resultaat terug

We maken deze template beschikbaar voor gebruik door een berichtlistener in te stellen die commando's van de extensiepagina accepteert. We gebruiken het doorgegeven command om te bepalen wat er moet gebeuren (je kunt je voorstellen dat er meer moet gebeuren dan alleen renderen; misschien templates maken? Of ze op een of andere manier beheren?), en de context wordt direct aan de template doorgegeven voor rendering. 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 ontvangen we dit bericht en doen we iets interessants met de html gegevens die we hebben ontvangen. In dit geval geven we de gegevens gewoon weer via een notificatie , maar het is absoluut mogelijk om deze HTML veilig te gebruiken als onderdeel van de gebruikersinterface van de extensie. Het invoegen ervan via innerHTML vormt geen significant beveiligingsrisico, omdat we vertrouwen hebben in de inhoud die binnen de sandbox is weergegeven.

Dit mechanisme maakt het maken van sjablonen eenvoudig, maar het is natuurlijk niet beperkt tot sjablonen. Alle code die niet direct werkt onder een strikt Content Security Policy kan in een sandbox worden geplaatst; het is zelfs vaak nuttig om componenten van je extensies die wel correct zouden werken in een sandbox te plaatsen om elk onderdeel van je programma te beperken tot de minimaal noodzakelijke privileges voor een goede uitvoering. De presentatie ' Writing Secure Web Apps and Chrome Extensions' van Google I/O 2012 geeft goede voorbeelden van deze techniek in de praktijk en is zeker de moeite waard om 56 minuten van je tijd te bekijken.