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 semplici: lo script deve essere spostato fuori linea in file JavaScript distinti, i gestori di eventi in linea devono essere convertiti per utilizzare addEventListener e eval() deve essere disabilitato.

Tuttavia, siamo consapevoli che una serie di librerie utilizzano costrutti simili a eval() e eval, come new Function(), per l'ottimizzazione delle prestazioni 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 di questa funzionalità si è quindi dimostrata più problematica 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 avere un impatto significativo 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 codice senza accedere ai dati o alle API di alto valore dell'estensione. Nessun dato, nessuna API, nessun problema.

Per farlo, elenchiamo file HTML specifici all'interno del pacchetto dell'estensione come in 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 in sandbox nella nostra estensione tramite un iframe, possiamo passarle messaggi, consentirle di intervenire in qualche modo su questi messaggi e attendere che ci restituisca un risultato. 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 maggiori spiegazioni, 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 ed è facile dimenticarlo, quindi verifica che il file in sandbox sia elencato nel file manifest. In questo esempio, stiamo eseguendo la sandbox del file astutamente chiamato "sandbox.html". La voce manifest ha il seguente aspetto:

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

Carica il file in sandbox

Per fare qualcosa di interessante con il file in sandbox, dobbiamo caricarlo in un contesto in cui possa essere indirizzato dal codice dell'estensione. In questo caso, il file sandbox.html è stato caricato in una pagina dell'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

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 va a buon fine. Anche se Handlebars.compile finisce per utilizzare new Function, tutto funziona esattamente come previsto e otteniamo 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? Forse gestirli in qualche modo?), e context verrà passato 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>

Nella pagina dell'estensione, riceveremo questo messaggio e faremo qualcosa di interessante con i html dati che ci sono stati passati. In questo caso, lo ripeteremo tramite una notifica, ma è del tutto possibile utilizzare questo codice HTML in sicurezza nell'interfaccia utente dell'estensione. L'inserimento tramite innerHTML non comporta un rischio significativo per la sicurezza, in quanto riteniamo attendibili i contenuti visualizzati all'interno della sandbox.

Questo meccanismo semplifica la creazione di modelli, ma non è limitato a questo. 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.