Utilizzo di eval() in iframe con sandbox

Il sistema di estensioni di Chrome applica un Criterio di sicurezza del contenuto (CSP) predefinito piuttosto rigoroso. Le restrizioni dei criteri sono semplici: lo script deve essere spostato fuori riga in file JavaScript separati, i gestori di eventi in linea devono essere convertiti per utilizzare addEventListener e eval() è disabilitato.

Riconosciamo, tuttavia, che una varietà di librerie utilizza eval() e eval-like costrutti come new Function() per l'ottimizzazione delle prestazioni e la facilità di espressione. Le librerie di modelli sono particolarmente soggette a questo stile di implementazione. Sebbene alcune (come Angular.js) supportino il CSP out of the box, molti framework popolari non sono ancora stati aggiornati a un meccanismo compatibile con il mondo senza eval delle estensioni. La rimozione del supporto per questa funzionalità si è quindi rivelata più problematica del previsto per gli sviluppatori.

Questo documento introduce il sandboxing come meccanismo sicuro per includere queste librerie nei progetti senza compromettere la sicurezza.

Perché il sandbox?

eval è pericoloso all'interno di un'estensione perché il codice che esegue ha accesso a tutto nell'ambiente con autorizzazioni elevate dell'estensione. Sono disponibili una serie di potenti API chrome.* che potrebbero influire gravemente sulla sicurezza e sulla privacy di un utente; la semplice esfiltrazione dei dati è la preoccupazione minore. La soluzione offerta è un sandbox in cui eval può eseguire il codice senza accedere ai dati dell'estensione o alle API di alto valore dell'estensione. Nessun dato, nessuna API, nessun problema.

Per farlo, elenchiamo i file HTML specifici all'interno del pacchetto di estensioni come sandbox. Ogni volta che viene caricata una pagina in sandbox, viene spostata in un'origine univoca e le viene negato l'accesso alle API chrome.*. Se carichiamo questa pagina in sandbox nella nostra estensione tramite un iframe, possiamo passargli messaggi, lasciarlo agire in qualche modo su questi messaggi e attendere che ci restituisca un risultato. Questo semplice meccanismo di messaggistica ci fornisce tutto ciò di cui abbiamo bisogno per includere in modo sicuro il codice basato su eval nel flusso di lavoro della nostra estensione.

Creare e utilizzare un sandbox

Se vuoi passare direttamente al codice, scarica l'estensione di esempio del sandboxing e inizia. È un esempio funzionante di una piccola API di messaggistica basata sulla libreria di modelli Handlebars e dovrebbe fornirti tutto ciò di cui hai bisogno per iniziare. Per chi desidera una spiegazione più dettagliata, esaminiamo insieme questo esempio.

Elencare i file nel manifest

Ogni file che deve essere eseguito all'interno di un sandbox deve essere elencato nel manifest dell'estensione aggiungendo una proprietà sandbox. Questo è un passaggio fondamentale ed è facile da dimenticare, quindi controlla che il file in sandbox sia elencato nel manifest. In questo esempio, stiamo mettendo in sandbox il file denominato "sandbox.html". La voce del manifest è simile alla seguente:

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

Caricare il file in sandbox

Per fare qualcosa di interessante con il file in sandbox, dobbiamo caricarlo in un contesto in cui il codice dell'estensione possa indirizzarlo. In questo caso, sandbox.html è stato caricato in una pagina di estensione utilizzando un iframe. Il file JavaScript della pagina contiene codice che invia un messaggio al sandbox ogni volta che l'utente fa clic sull'azione del browser, trovando l'iframe nella pagina e chiamando postMessage() sul relativo contentWindow. Il messaggio è un oggetto contenente tre proprietà: context, templateName e command. Tra poco approfondiremo context e command.

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

Fare qualcosa di pericoloso

Quando viene caricato sandbox.html, viene caricata la libreria Handlebars e viene creato e compilato un modello in linea nel modo suggerito da Handlebars:

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>

Non si verifica alcun errore. Anche se Handlebars.compile finisce per utilizzare new Function, le cose funzionano esattamente come previsto e otteniamo un modello compilato in templates['hello'].

Restituire il risultato

Renderemo questo modello disponibile per l'uso configurando un listener di messaggi che accetta i comandi dalla pagina dell'estensione. Utilizzeremo il command passato per determinare cosa deve essere fatto (potresti immaginare di fare qualcosa di più della semplice rendering; magari creare modelli? Forse gestirli in qualche modo?) e il context verrà passato direttamente al modello per il rendering. L'HTML sottoposto a rendering verrà restituito alla pagina dell'estensione in modo che l'estensione possa farne un uso utile in un secondo momento:

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

Nella pagina dell'estensione, riceveremo questo messaggio e faremo qualcosa di interessante con i dati html che ci sono stati passati. In questo caso, lo riporteremo semplicemente tramite una notifica, ma è del tutto possibile utilizzare questo HTML in modo sicuro come parte dell'interfaccia utente dell'estensione. L'inserimento tramite innerHTML non comporta un rischio per la sicurezza significativo, in quanto ci fidiamo dei contenuti sottoposti a rendering all'interno del sandbox.

Questo meccanismo semplifica la creazione di modelli, ma ovviamente non si limita a questo. Qualsiasi codice che non funziona out-of-the-box con un Criterio di sicurezza del contenuto rigoroso può essere messo in sandbox; in effetti, spesso è utile mettere in sandbox i componenti delle estensioni che verrebbero eseguiti correttamente per limitare ogni parte del programma al set di privilegi più piccolo necessario per la sua corretta esecuzione. La presentazione Writing Secure Web Apps and Chrome Extensions di Google I/O 2012 fornisce alcuni buoni esempi di questa tecnica in azione e vale la pena dedicare 56 minuti del tuo tempo.