Il sistema di estensioni di Chrome applica un Content Security Policy (CSP) predefinito piuttosto rigoroso.
Le limitazioni dei criteri sono semplici: lo script deve essere spostato fuori linea in file JavaScript separati, i gestori di eventi in linea devono essere convertiti per utilizzare addEventListener e eval() è disattivato.
Tuttavia, ci rendiamo conto che varie librerie utilizzano costrutti simili a eval() e eval, ad esempio
new Function(), per l'ottimizzazione delle prestazioni e la facilità di espressione. Le librerie di modelli sono
particolarmente soggette a questo stile di implementazione. Mentre alcuni (come Angular.js) supportano 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 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é la sandbox?
eval è pericoloso all'interno di un'estensione perché il codice che esegue ha accesso a tutto ciò che si trova nell'ambiente con privilegi elevati dell'estensione. Sono disponibili una serie di potenti API browser.* che potrebbero
influire gravemente sulla sicurezza e sulla privacy di un utente; la semplice esfiltrazione dei dati è il minimo 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 questo scopo, elenchiamo file HTML specifici all'interno del pacchetto di estensione come in sandbox.
Ogni volta che viene caricata una pagina in sandbox, questa viene spostata in un'origine univoca e le viene negato l'accesso alle API browser.*. Se carichiamo questa pagina in sandbox nella nostra estensione tramite un iframe, possiamo
passarle messaggi, consentirle di agire su questi messaggi in qualche modo 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 una sandbox
Se vuoi passare subito al codice, scarica l'estensione di esempio di sandboxing 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 volesse una spiegazione più dettagliata, esaminiamo insieme questo esempio.
Elenca i file nel manifest
Ogni file che deve essere eseguito all'interno di una sandbox deve essere elencato nel manifest dell'estensione aggiungendo una proprietà
sandbox. Questo è un passaggio fondamentale ed è facile dimenticarlo, quindi verifica che
il file in sandbox sia elencato nel manifest. In questo esempio, eseguiamo il sandboxing del file
denominato in modo intelligente "sandbox.html". La voce del 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, sandbox.html è stato caricato in
una pagina di estensione utilizzando 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
che contiene tre proprietà: context, templateName e command. Tra poco parleremo di context e command.
service-worker.js:
browser.action.onClicked.addListener(() => {
browser.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, carica la libreria Handlebars e crea e compila un modello
inline 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>
Questo non fallisce! Anche se Handlebars.compile finisce per utilizzare new Function, le cose funzionano
esattamente come previsto e otteniamo un modello compilato in templates['hello'].
Passa il risultato
Rendiamo disponibile questo modello 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 di più del semplice rendering, magari creare modelli? forse gestirli in qualche modo?) e context verrà passato direttamente nel modello per il rendering. L'HTML di cui è stato eseguito il 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 inviati. In questo caso, lo riporteremo 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 significativo per la sicurezza, in quanto consideriamo attendibili i contenuti visualizzati
all'interno della sandbox.
Questo meccanismo semplifica la creazione di modelli, ma ovviamente non si limita a questo. Qualsiasi codice che non funziona immediatamente in base a una Content Security Policy rigorosa può essere messo in sandbox. Infatti, spesso è utile mettere in sandbox i componenti delle estensioni che funzionerebbero correttamente per limitare ogni parte del programma al più piccolo insieme di privilegi necessari per la sua corretta esecuzione. La presentazione Writing Secure Web Apps and Chrome Extensions di Google I/O 2012 fornisce alcuni buoni esempi di queste tecniche in azione e vale la pena di dedicare 56 minuti del tuo tempo.