Utilizzo di eval() in iframe con sandbox

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

Tuttavia, siamo consapevoli che varie librerie utilizzano costrutti eval() e eval 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 alcuni (come Angular.js) supportino i CSP da subito, molti framework popolari non sono ancora stati aggiornati a un meccanismo compatibile con il mondo senza eval delle estensioni. Pertanto, la rimozione del supporto per questa funzionalità si è rivelata più problematica del previsto per gli sviluppatori.

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

Perché usare la sandbox?

eval è pericoloso all'interno di un'estensione perché il codice che esegue ha accesso a tutti gli elementi presenti nell'ambiente con autorizzazioni elevate dell'estensione. Sono disponibili molte API chrome.* potenti che potrebbero avere un impatto significativo sulla sicurezza e sulla privacy di un utente; la semplice esfiltrazione di dati è l'ultimo dei nostri problemi. La soluzione offerta è una sandbox in cui eval può eseguire codice senza accedere ai dati dell'estensione o alle API di alto valore dell'estensione. Nessun dato, nessuna API, nessun problema.

A tal fine, elenchiamo file HTML specifici all'interno del pacchetto dell'estensione come sandbox. Ogni volta che viene caricata una pagina con sandbox, questa viene spostata in un'origine univoca e l'accesso alle API chrome.* sarà negato. Se carichiamo questa pagina con sandbox nella nostra estensione tramite un iframe, possiamo passare i messaggi, consentirgli di agire su quei messaggi in qualche modo e attendere che restituisca un risultato. Questo semplice meccanismo di messaggistica ci offre 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 una sandbox

Se vuoi approfondire direttamente il codice, prendi l'estensione di esempio per il sandboxing e inizia. Si tratta di un esempio funzionante di una piccola API di messaggistica basata sulla libreria di modelli Handlebars che dovrebbe fornirti tutto ciò di cui hai bisogno per iniziare. Per chi desidera una spiegazione più approfondita, vediamo insieme l'esempio.

Elenca file nel file manifest

Ogni file che deve essere eseguito all'interno di una sandbox deve essere elencato nel manifest dell'estensione aggiungendo una proprietà sandbox. Si tratta di un passaggio fondamentale ed è facile da dimenticare, quindi verifica attentamente che il file sandbox sia elencato nel manifest. In questo esempio, eseguiamo il sandboxing del file denominato "sandbox.html". La voce del file manifest ha il seguente aspetto:

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

Carica il file con sandbox

Per poter realizzare qualcosa di interessante con il file sandbox, dobbiamo caricarlo in un contesto in cui può essere risolto con il codice dell'estensione. Qui sandbox.html è stato caricato in una pagina di estensione tramite iframe. Il file JavaScript della pagina contiene il codice che invia un messaggio alla sandbox ogni volta che un utente fa clic sull'azione del browser individuando il iframe nella pagina e chiamando postMessage() sul relativo contentWindow. Il messaggio è un oggetto con tre proprietà: context, templateName e command. Approfondiremo context e command tra un attimo.

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

Fai qualcosa di pericoloso

Una volta caricato, sandbox.html carica la libreria Handlebars e crea e compila un modello incorporato nel modo in cui Handlebars suggerisce:

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>

Questa operazione non ha esito negativo. Anche se in Handlebars.compile viene usato new Function, tutto funziona esattamente come previsto e viene creato un modello compilato in templates['hello'].

Trasmetti il risultato

Renderemo disponibile l'utilizzo di questo modello impostando un listener di messaggi che accetti i comandi dalla pagina dell'estensione. Utilizzeremo il codice command trasmesso per determinare cosa bisognerebbe fare (potresti immaginare di fare qualcosa di più del semplice rendering, magari creare modelli?) Se gestiscile in qualche modo?) e il valore context verrà trasferito direttamente nel modello per il rendering. L'HTML sottoposto a rendering viene restituito alla pagina dell'estensione in modo che l'estensione possa utilizzarlo in seguito:

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

Tornando alla pagina dell'estensione, riceverai questo messaggio e faremo qualcosa di interessante con i dati html che ci sono stati trasmessi. In questo caso, utilizzeremo semplicemente l'eco tramite una notifica, ma è possibile utilizzare questo codice HTML in sicurezza nell'interfaccia utente dell'estensione. L'inserimento tramite innerHTML non rappresenta un rischio per la sicurezza significativo perché riteniamo attendibili i contenuti visualizzati all'interno della sandbox.

Questo meccanismo rende i modelli semplici, ma non si limita ai modelli. Qualsiasi codice che non funziona subito in base a rigorosi criteri di sicurezza del contenuto può essere limitato tramite sandbox; in effetti, è spesso utile limitare la sandbox dei componenti delle estensioni che venrebbero eseguiti correttamente per limitare ogni parte del programma al minor numero di privilegi necessari per una corretta esecuzione. La presentazione Scrivere app web sicure ed estensioni di Chrome di Google I/O 2012 offre alcuni buoni esempi di questa tecnica in pratica e vale 56 minuti del vostro tempo.