Das Erweiterungssystem von Chrome erzwingt eine ziemlich strenge standardmäßige Content Security Policy (CSP).
Die Richtlinieneinschränkungen sind einfach: Das Skript muss in separate JavaScript-Dateien verschoben und Inline-Event-Handler zur Verwendung von addEventListener
konvertiert werden. eval()
ist deaktiviert.
Uns ist jedoch bewusst, dass bei einer Vielzahl von Bibliotheken eval()
- und eval
-ähnliche Konstrukte wie new Function()
zur Leistungsoptimierung und einfachen Ausdrucksweise verwendet werden. Vorlagenbibliotheken sind für diesen Implementierungsstil besonders anfällig. Einige (wie Angular.js) unterstützen CSP, aber viele beliebte Frameworks wurden noch nicht auf einen Mechanismus aktualisiert, der mit der Welt ohne eval
kompatibel ist. Das Entfernen der Unterstützung für diese Funktionen hat sich für Entwickler daher als problematischer erwiesen.
In diesem Dokument wird das Sandboxing als sicherer Mechanismus vorgestellt, um diese Bibliotheken in Ihre Projekte einzubinden, ohne die Sicherheit zu beeinträchtigen.
Warum Sandbox?
eval
ist in einer Erweiterung gefährlich, da der ausgeführte Code Zugriff auf alles in der Umgebung mit hohen Berechtigungen der Erweiterung hat. Es gibt eine Vielzahl von leistungsstarken chrome.*
APIs, die die Sicherheit und den Datenschutz eines Nutzers erheblich beeinträchtigen können. Eine einfache Daten-Exfiltration ist für uns kein Problem.
Die angebotene Lösung ist eine Sandbox, in der eval
Code ausführen kann, ohne entweder auf die Daten der Erweiterung oder die hochwertigen APIs der Erweiterung zuzugreifen. Keine Daten, keine APIs, kein Problem.
Dies erreichen wir, indem wir bestimmte HTML-Dateien im Erweiterungspaket als in einer Sandbox ausgeführte Dateien auflisten.
Immer wenn eine Seite geladen wird, die in einer Sandbox ausgeführt wird, wird sie an einen eindeutigen Ursprung verschoben und ihnen wird der Zugriff auf chrome.*
-APIs verweigert. Wenn wir diese Sandbox-Seite über einen iframe
in unsere Erweiterung laden, können wir Nachrichten an sie übergeben, sie in irgendeiner Form auf diese Nachrichten reagieren lassen und warten, bis sie ein Ergebnis zurückgibt. Dieser einfache Messaging-Mechanismus bietet uns alles, was wir benötigen, um eval
-gesteuerten Code sicher in den Workflow unserer Erweiterung aufzunehmen.
Sandbox erstellen und verwenden
Wenn Sie direkt mit dem Programmieren beginnen möchten, greifen Sie auf die Sandboxing-Beispielerweiterung zurück. Das ist ein funktionierendes Beispiel für eine kleine Messaging-API, die auf der Vorlagenbibliothek Handlebars aufbaut und Ihnen alles bietet, was Sie für den Anfang brauchen. Diejenigen unter Ihnen, die etwas mehr darüber erfahren möchten, gehen wir das Beispiel hier durch.
Dateien im Manifest auflisten
Jede Datei, die in einer Sandbox ausgeführt werden soll, muss im Erweiterungsmanifest durch Hinzufügen des Attributs sandbox
aufgeführt werden. Dies ist ein wichtiger Schritt, der leicht vergessen werden kann. Prüfen Sie daher, ob Ihre in der Sandbox ausgeführte Datei im Manifest aufgeführt ist. In diesem Beispiel wird die Datei namens "sandbox.html" in einer Sandbox ausgeführt. Der Manifesteintrag sieht so aus:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
In der Sandbox ausgeführte Datei laden
Um etwas Interessantes mit der Sandbox-Datei zu machen, müssen wir sie in einem Kontext laden, in dem sie vom Code der Erweiterung angesprochen werden kann. Hier wurde sandbox.html über iframe
in eine Erweiterungsseite geladen. Die JavaScript-Datei der Seite enthält Code, der bei jedem Klick auf die Browseraktion eine Nachricht an die Sandbox sendet. Dazu wird das iframe
auf der Seite gesucht und postMessage()
für seine contentWindow
aufgerufen. Die Nachricht ist ein Objekt mit drei Eigenschaften: context
, templateName
und command
. Wir sehen uns gleich context
und command
genauer an.
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, '*');
});
Etwas Gefährliches unternehmen
Wenn sandbox.html
geladen wird, wird die Handlebars-Bibliothek geladen und eine Inline-Vorlage erstellt und kompiliert, wie von Handlebars vorgeschlagen:
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>
Das ist kein Scheitern! Obwohl Handlebars.compile
am Ende new Function
verwendet, funktioniert alles genau wie erwartet und wir haben am Ende eine kompilierte Vorlage in templates['hello']
.
Ergebnis zurücksenden
Wir stellen diese Vorlage zur Verfügung, indem wir einen Nachrichten-Listener einrichten, der Befehle von der Erweiterungsseite akzeptiert. Wir verwenden das übergebene command
, um zu bestimmen, was zu tun ist. (Sie könnten sich mehr vorstellen, als nur zu rendern. Vielleicht könnten Sie Vorlagen erstellen? Verwalten Sie sie vielleicht auf irgendeine Weise?) und context
wird zum Rendern direkt an die Vorlage übergeben. Der gerenderte HTML-Code wird an die Erweiterungsseite zurückgegeben, damit die Erweiterung später nützliche Funktionen nutzen kann:
<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>
Auf der Seite mit den Erweiterungen erhalten wir diese Nachricht und machen etwas Interessantes mit den html
-Daten, die uns übergeben wurden. In diesem Fall wird der HTML-Code einfach über eine Benachrichtigung wiedergegeben. Es ist jedoch durchaus möglich, den HTML-Code sicher als Teil der Benutzeroberfläche der Erweiterung zu verwenden. Das Einfügen über innerHTML
stellt kein nennenswertes Sicherheitsrisiko dar, da wir den Inhalten vertrauen, die in der Sandbox gerendert wurden.
Dieser Mechanismus macht das Erstellen von Vorlagen einfach, ist aber natürlich nicht auf Vorlagen beschränkt. Jeder Code, der im Rahmen einer strengen Content Security Policy nicht sofort einsatzbereit ist, kann in einer Sandbox ausgeführt werden. Tatsächlich ist es oft nützlich, Komponenten Ihrer Erweiterungen, die ordnungsgemäß ausgeführt würden, in einer Sandbox auszuführen. So begrenzen Sie jedes Teil Ihres Programms auf den kleinsten Berechtigungssatz, der für eine ordnungsgemäße Ausführung erforderlich ist. Die Präsentation Writing Secure Web Apps and Chrome Extensions von Google I/O 2012 enthält einige gute Beispiele für diese Techniken in der Praxis und ist 56 Minuten Ihrer Zeit wert.