Le système d'extensions de Chrome applique une Content Security Policy (CSP) par défaut assez stricte.
Les restrictions de la règle sont simples : le script doit être déplacé hors ligne dans des fichiers JavaScript distincts, les gestionnaires d'événements intégrés doivent être convertis pour utiliser addEventListener, et eval() est désactivé.
Nous reconnaissons toutefois qu'un certain nombre de bibliothèques utilisent eval() et des constructions de type eval, telles que
new Function(), pour optimiser les performances et faciliter l'expression. Les bibliothèques de modèles sont particulièrement sujettes à ce style d'implémentation. Bien que certaines (comme Angular.js) soient compatibles avec la CSP prêtes à l'emploi, de nombreux frameworks populaires n'ont pas encore été mis à jour vers un mécanisme compatible avec le monde sans eval des extensions. La suppression de la compatibilité avec cette fonctionnalité s'est donc avérée plus
problématique que prévu pour les développeurs.
Ce document présente le bac à sable comme un mécanisme sécurisé permettant d'inclure ces bibliothèques dans vos projets sans compromettre la sécurité.
Pourquoi utiliser un bac à sable ?
eval est dangereux dans une extension, car le code qu'il exécute a accès à tout ce qui se trouve dans l'environnement à autorisation élevée de l'extension. De nombreuses API chrome.* puissantes sont disponibles et peuvent avoir un impact considérable sur la sécurité et la confidentialité d'un utilisateur. L'exfiltration de données simple est le moindre de nos soucis.
La solution proposée est un bac à sable dans lequel eval peut exécuter du code sans accéder aux données de l'extension ni à ses API à forte valeur ajoutée. Pas de données, pas d'API, pas de problème.
Pour ce faire, nous listons des fichiers HTML spécifiques dans le package d'extension comme étant mis en bac à sable.
Chaque fois qu'une page mise en bac à sable est chargée, elle est déplacée vers une origine unique et l'accès aux
API chrome.* lui est refusé. Si nous chargeons cette page mise en bac à sable dans notre extension via un iframe, nous pouvons lui transmettre des messages, la laisser agir sur ces messages d'une manière ou d'une autre, et attendre qu'elle nous renvoie un résultat. Ce mécanisme de messagerie simple nous donne tout ce dont nous avons besoin pour inclure en toute sécurité du code piloté par eval dans le workflow de notre extension.
Créer et utiliser un bac à sable
Si vous souhaitez accéder directement au code, récupérez l'exemple d'extension de bac à sable et lancez-vous. Il s'agit d'un exemple de fonctionnement d'une petite API de messagerie basée sur la bibliothèque de modèles Handlebars . Elle devrait vous fournir tout ce dont vous avez besoin pour commencer. Pour ceux qui souhaitent obtenir plus d'explications, examinons ensemble cet exemple.
Lister les fichiers dans le fichier manifeste
Chaque fichier qui doit être exécuté dans un bac à sable doit être listé dans le fichier manifeste de l'extension en ajoutant une propriété sandbox. Il s'agit d'une étape essentielle, et il est facile de l'oublier. Vérifiez donc que votre fichier mis en bac à sable est listé dans le fichier manifeste. Dans cet exemple, nous mettons en bac à sable le fichier nommé "sandbox.html". L'entrée du fichier manifeste se présente comme suit :
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Charger le fichier mis en bac à sable
Pour faire quelque chose d'intéressant avec le fichier mis en bac à sable, nous devons le charger dans un contexte où il peut être adressé par le code de l'extension. Ici, sandbox.html a été chargé dans une page d'extension à l'aide d'un iframe. Le fichier JavaScript de la page contient du code qui envoie un message dans le bac à sable chaque fois que l'action du navigateur est cliquée en recherchant l'iframe sur la page et en appelant postMessage() sur son contentWindow. Le message est un objet contenant trois propriétés : context, templateName et command. Nous allons examiner context et command dans un instant.
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, '*');
});
Faire quelque chose de dangereux
Lorsque sandbox.html est chargé, il charge la bibliothèque Handlebars, puis crée et compile un modèle intégré de la manière suggérée par 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>
Cela ne fonctionne pas ! Même si Handlebars.compile finit par utiliser new Function, tout fonctionne
exactement comme prévu et nous obtenons un modèle compilé dans templates['hello'].
Transmettre le résultat
Nous allons rendre ce modèle disponible en configurant un écouteur de messages qui accepte les commandes de la page d'extension. Nous utiliserons la command transmise pour déterminer ce qui doit être fait (vous pouvez imaginer faire plus que simplement effectuer le rendu, par exemple créer des modèles ? Ou les gérer d'une manière ou d'une autre ?), et le context sera transmis directement au modèle pour le rendu. Le code HTML rendu sera transmis à la page d'extension afin que l'extension puisse l'utiliser ultérieurement :
<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>
De retour sur la page d'extension, nous recevrons ce message et ferons quelque chose d'intéressant avec les données html qui nous ont été transmises. Dans ce cas, nous allons simplement les afficher via une notification, mais
il est tout à fait possible d'utiliser ce code HTML en toute sécurité dans l'interface utilisateur de l'extension. L'insertion via innerHTML ne présente pas de risque de sécurité important, car nous faisons confiance au contenu qui a été rendu dans le bac à sable.
Ce mécanisme simplifie la création de modèles, mais il ne se limite pas à cela. Tout code qui ne fonctionne pas prêt à l'emploi dans le cadre d'une Content Security Policy stricte peut être mis en bac à sable. En fait, il est souvent utile de mettre en bac à sable les composants de vos extensions qui fonctionneraient correctement afin de limiter chaque partie de votre programme au plus petit ensemble de privilèges nécessaires à son exécution correcte. La présentation Writing Secure Web Apps and Chrome Extensions (Écrire des applications Web et des extensions Chrome sécurisées) de Google I/O 2012 fournit de bons exemples de ces techniques en action et vaut la peine d'être visionnée (56 minutes de votre temps).