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 limitazioni dei criteri sono chiare: lo script deve essere spostato fuori riga in file separati I file JavaScript e i gestori di eventi incorporati devono essere convertiti per utilizzare addEventListener e eval() è disattivata.

Tuttavia, siamo consapevoli che una serie di librerie utilizzano costrutti simili a eval() e eval, come new Function(), per l'ottimizzazione del rendimento e la facilità di espressione. Le librerie di modelli sono particolarmente inclini a questo stile di implementazione. Sebbene alcuni (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 tale funzionalità si è quindi dimostrata maggiore problematici del previsto per gli sviluppatori.

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

Perché la sandbox?

eval è pericoloso all'interno di un'estensione perché il codice che esegue ha accesso a tutto nell'ambiente con autorizzazioni elevate dell'estensione. Esistono una serie di potenti API chrome.* che potrebbero incidere notevolmente sulla sicurezza e sulla privacy di un utente. La semplice esfiltrazione dei dati è il problema minore. La soluzione offerta è una sandbox in cui eval può eseguire il codice senza accedere i dati dell'estensione o le 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 sottoposti a sandbox. Ogni volta che viene caricata una pagina in sandbox, questa viene spostata in un'origine univoca e viene negato l'accesso alle API chrome.*. Se carichiamo questa pagina con sandbox nella nostra estensione tramite un iframe, possiamo trasmetterlo, lasciarlo agire in qualche modo su quei messaggi e attendere che ci ritrasmetta o il risultato finale. Questo semplice meccanismo di messaggistica ci offre tutto ciò di cui abbiamo bisogno per includere in sicurezza il codice basato su eval nel flusso di lavoro della nostra estensione.

Creare e utilizzare una sandbox

Se vuoi passare subito al codice, scarica l'estensione di esempio per la sandbox e inizia. Si tratta di 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 avesse bisogno di un'ulteriore spiegazione, esaminiamo insieme questo esempio.

Elenca i file nel manifest

Ogni file che deve essere eseguito in una sandbox deve essere elencato nel manifest dell'estensione aggiungendo una proprietà sandbox. Si tratta di un passaggio fondamentale, facile da dimenticare, quindi controlla bene il file con sandbox è elencato nel file manifest. In questo esempio, stiamo eseguendo il sandboxing del file abilmente denominato "sandbox.html". La voce del file manifest ha il seguente aspetto:

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

Carica il file in sandbox

Per poter fare qualcosa di interessante con il file con sandbox, dobbiamo caricarlo in un contesto in cui può essere affrontato dal codice dell'estensione. Qui sandbox.html è stato caricato pagina di un'estensione tramite un iframe. Il file JavaScript della pagina contiene codice che invia un messaggio alla sandbox ogni volta che viene fatto clic sull'azione del browser trovando iframe nella pagina e chiamando postMessage() sul relativo contentWindow. Il messaggio è un oggetto contenente tre proprietà: context, templateName e command. Tra poco parleremo di 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

Una volta caricato sandbox.html, carica la libreria Handlebars, quindi crea e compila una 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>

Non fallire! Anche se Handlebars.compile utilizza new Function, l'operazione funziona esattamente come previsto, per cui viene creato un modello compilato in templates['hello'].

Passare il risultato

Renderemo disponibile questo modello impostando un listener di messaggi che accetti i comandi dalla pagina dell'estensione. Utilizzeremo il parametro command passato per determinare cosa deve essere fatto (puoi immaginare di fare di più che semplicemente eseguire il rendering, forse creare modelli? Magari gestendole in alcune ?) e context verrà trasmesso direttamente al modello per il rendering. Il codice HTML visualizzato verrà restituito alla pagina dell'estensione in modo che l'estensione possa utilizzarlo 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>

Tornando alla pagina dell'estensione, riceveremo questo messaggio e faremo qualcosa di interessante nell'html che abbiamo trasmesso. In questo caso, lo ripeteremo tramite una notifica, ma è del tutto possibile utilizzare questo codice HTML in sicurezza nell'interfaccia utente dell'estensione. Inserendo il dispositivo tramite innerHTML non rappresenta un rischio significativo per la sicurezza perché confidiamo nel contenuto che è stato visualizzato all'interno della sandbox.

Questo meccanismo rende semplice la creazione di modelli, ma ovviamente non si limita alla creazione di modelli. Qualsiasi codice che non funziona immediatamente in base a norme di sicurezza dei contenuti rigorose può essere sottoposto a sandbox. In effetti, spesso è utile eseguire la sandbox dei componenti delle estensioni che verrebbero eseguiti correttamente per limitare ogni componente del programma al più piccolo insieme di privilegi necessari per l'esecuzione corretta. La presentazione Scrivere app web e estensioni di Chrome sicure del Google I/O 2012 fornisce alcuni buoni esempi di queste tecniche in azione e vale 56 minuti del tuo tempo.