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.