System rozszerzeń Chrome wymusza dość rygorystyczną domyślną politykę bezpieczeństwa treści (CSP).
Ograniczenia wynikające z zasad są proste: skrypt musi zostać przeniesiony poza wiersz do osobnych plików JavaScript, wbudowane moduły obsługi zdarzeń trzeba przekonwertować na korzystanie z elementu addEventListener
, a element eval()
musi być wyłączony.
Zdajemy sobie jednak sprawę, że różne biblioteki korzystają z konstrukcji typu eval()
i eval
, takich jak new Function()
, które ułatwiają optymalizację wydajności i korzystanie z nich. Szczególnie narażone są biblioteki szablonów
na taki sposób implementacji. Niektóre z nich (np. Angular.js) od razu po uruchomieniu obsługują CSP, ale wiele popularnych platform nie zostało jeszcze zaktualizowanych do mechanizmu kompatybilnego ze światem dostępnym tylko w eval
rozszerzeniach. Usunięcie obsługi tej funkcji okazało się więc większym problemem dla deweloperów niż się spodziewaliśmy.
W tym dokumencie opisujemy piaskownicę jako bezpieczny mechanizm pozwalający uwzględnić te biblioteki w projektach bez naruszania bezpieczeństwa.
Dlaczego piaskownica?
Element eval
jest niebezpieczny w rozszerzeniu, ponieważ uruchamiany przez niego kod ma dostęp do wszystkiego w środowisku o wysokich uprawnieniach rozszerzenia. Dostępnych jest wiele zaawansowanych interfejsów API chrome.*
, które mogą negatywnie wpłynąć na bezpieczeństwo i prywatność użytkowników. Nie musisz się martwić o proste wydobycie danych.
Oferowane rozwiązanie to piaskownica, w której eval
może uruchamiać kod bez dostępu do danych rozszerzenia ani do jego wysokiej wartości API. Nie ma danych, interfejsów API ani problemów.
W tym celu pokazujemy określone pliki HTML w pakiecie rozszerzeń jako pliki w piaskownicy.
Po każdym załadowaniu strony w piaskownicy jest ona przenoszona do unikalnego źródła i nie otrzymuje dostępu do interfejsów API chrome.*
. Jeśli wczytamy stronę znajdującą się w piaskownicy do naszego rozszerzenia za pomocą obiektu iframe
, możemy przekazać do niej komunikaty, pozwolić mu na jakąś czynność związaną z tymi wiadomościami i zaczekać, aż zwróci wynik. Ten prosty mechanizm komunikacji daje nam wszystko, czego potrzebujemy, aby bezpiecznie umieścić w procesie rozszerzenia kod oparty na eval
.
Tworzenie piaskownicy i korzystanie z niej
Jeśli chcesz od razu przejść do kodu, pobierz przykładowe rozszerzenie piaskownicy i zacznij działać. To działający przykład małego interfejsu API do przesyłania wiadomości, który jest oparty na bibliotece szablonów Handlebars i powinno zawierać wszystko, czego potrzebujesz. Jeśli szukasz dokładniejszych wyjaśnień, omówię ten fragment.
Wyświetlenie listy plików w pliku manifestu
Każdy plik, który powinien być uruchomiony wewnątrz piaskownicy, musi być wymieniony w pliku manifestu rozszerzeń przez dodanie właściwości sandbox
. Jest to kluczowy krok, który łatwo jest zapomnieć. Dlatego sprawdź dokładnie, czy plik piaskownicy znajduje się w pliku manifestu. W tym przykładzie dzielimy na piaskownice plik o nazwie „sandbox.html”. Wpis w pliku manifestu wygląda tak:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Wczytaj plik piaskownicy
Aby można było zrobić coś ciekawego z plikiem w trybie piaskownicy, musimy go załadować w kontekście, w którym można mu to umożliwić za pomocą kodu rozszerzenia. W tym przypadku plik sandbox.html został wczytany
na stronę rozszerzenia przez iframe
. Plik JavaScript strony zawiera kod, który po każdym kliknięciu działania przeglądarki wysyła wiadomość do piaskownicy. Aby to zrobić, znajduje obiekt iframe
na stronie i wywołuje metodę postMessage()
za pomocą jej contentWindow
. Wiadomość jest obiektem zawierającym 3 właściwości: context
, templateName
i command
. Za chwilę omówimy context
i 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, '*');
});
Zrób coś niebezpiecznego
Po wczytaniu elementu sandbox.html
wczytuje się bibliotekę Handlebars, a następnie tworzy i kompiluje wbudowany szablon w sposób sugerujący, że:
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>
To nie koniec! Chociaż Handlebars.compile
kończy się używaniem new Function
, wszystko działa dokładnie zgodnie z oczekiwaniami – w efekcie otrzymujemy skompilowany szablon w templates['hello']
.
Przekaż wynik z powrotem
Udostępnimy ten szablon za pomocą odbiornika, który akceptuje polecenia ze strony rozszerzenia. Użyjemy przekazanego pliku command
, aby określić, co należy zrobić (wyobrażasz sobie coś więcej niż tylko renderowanie; np. tworzenie szablonów?). Być może jako zarządzanie nimi?), a element context
będzie przekazywany bezpośrednio do szablonu na potrzeby renderowania. Wyrenderowany kod HTML zostanie zwrócony na stronę rozszerzenia, dzięki czemu później będzie ono mogło:
<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>
Po powrocie na stronę rozszerzenia otrzymamy tę wiadomość i zrobimy coś interesującego z przesłanymi danymi html
. W tym przypadku odczytamy go w powiadomieniu, ale jest też możliwe bezpieczne użycie kodu HTML jako części interfejsu rozszerzenia. Wstawienie go za pomocą innerHTML
nie stwarza istotnego zagrożenia dla bezpieczeństwa, ponieważ uważamy treści wyrenderowane w piaskownicy za zaufane.
Ten mechanizm ułatwia tworzenie szablonów, ale oczywiście nie ogranicza się do nich. Każdy kod, który nie działa od razu w ramach ścisłej polityki bezpieczeństwa treści, może znajdować się w piaskownicy. W rzeczywistości takie rozwiązanie często sprawdza się w piaskownicy komponentów rozszerzeń, które były prawidłowo uruchamiane. Dzięki temu każdy element programu ma jak najmniejszy zestaw uprawnień niezbędnych do jego prawidłowego wykonania. Prezentacja na temat pisania bezpiecznych aplikacji internetowych i rozszerzeń do Chrome z Google I/O 2012 zawiera kilka przykładów tych technik w praktyce i warto poświęcić na nie 56 minut.