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.