System rozszerzeń Chrome wymusza dość rygorystyczną domyślną politykę bezpieczeństwa treści (CSP).
Ograniczenia zasad są proste: skrypt musi zostać przeniesiony z wiersza do osobnych plików JavaScript, wbudowane moduły obsługi zdarzeń muszą zostać przekonwertowane na używanie addEventListener, a eval() jest wyłączone.
Zdajemy sobie jednak sprawę, że różne biblioteki używają konstrukcji podobnych do eval() i eval, np. new Function(), aby optymalizować wydajność i ułatwiać wyrażanie. Biblioteki szablonów są szczególnie podatne na ten styl implementacji. Niektóre z nich (np. Angular.js) obsługują CSP od razu, ale wiele popularnych platform nie zostało jeszcze zaktualizowanych do mechanizmu zgodnego ze światem rozszerzeń bez eval. Dlatego usunięcie obsługi tej funkcji okazało się dla deweloperów bardziej problematyczne niż oczekiwano.
W tym dokumencie przedstawiamy piaskownicę jako bezpieczny mechanizm, który umożliwia uwzględnianie tych bibliotek w projektach bez obniżania poziomu bezpieczeństwa.
Dlaczego warto korzystać z piaskownicy?
eval jest niebezpieczne w rozszerzeniu, ponieważ wykonywany przez nie kod ma dostęp do wszystkich elementów środowiska rozszerzenia o wysokich uprawnieniach. Dostępnych jest wiele zaawansowanych interfejsów API browser.*, które mogą poważnie wpłynąć na bezpieczeństwo i prywatność użytkownika. Proste wycieki danych to najmniejszy z naszych problemów.
Oferowane rozwiązanie to piaskownica, w której eval może wykonywać kod bez dostępu do danych rozszerzenia ani do jego ważnych interfejsów API. Brak danych, brak interfejsów API, brak problemów.
Osiągamy to, umieszczając w pakiecie rozszerzenia listę konkretnych plików HTML, które są objęte piaskownicą.
Za każdym razem, gdy wczytywana jest strona w piaskownicy, jest ona przenoszona do unikalnego pochodzenia i odmawia się jej dostępu do interfejsów API browser.*. Jeśli załadujemy tę stronę w piaskownicy do naszego rozszerzenia za pomocą elementu iframe, możemy przekazywać jej wiadomości, pozwalać jej na podejmowanie działań na podstawie tych wiadomości i czekać, aż przekaże nam wynik. Ten prosty mechanizm przesyłania wiadomości zapewnia nam wszystko, czego potrzebujemy, aby bezpiecznie uwzględnić 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 do piaskownicy i zacznij pracę. Jest to działający przykład małego interfejsu API do przesyłania wiadomości, który został utworzony na podstawie biblioteki szablonów Handlebars. Powinien on zawierać wszystko, czego potrzebujesz, aby zacząć. Jeśli chcesz dowiedzieć się więcej, przeanalizujmy razem ten przykład.
Wyświetlanie listy plików w pliku manifestu
Każdy plik, który ma być uruchamiany w piaskownicy, musi być wymieniony w pliku manifestu rozszerzenia przez dodanie właściwości sandbox. To ważny krok, o którym łatwo zapomnieć, więc sprawdź, czy plik w piaskownicy jest wymieniony w manifeście. W tym przykładzie umieszczamy w piaskownicy plik o sprytnie dobranej nazwie „sandbox.html”. Wpis w pliku manifestu wygląda tak:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Wczytywanie pliku w piaskownicy
Aby zrobić coś ciekawego z plikiem w piaskownicy, musimy załadować go w kontekście, w którym kod rozszerzenia może się do niego odwoływać. W tym przypadku plik sandbox.html został wczytany na stronę rozszerzenia za pomocą elementu iframe. Plik JavaScript strony zawiera kod, który wysyła komunikat do piaskownicy za każdym razem, gdy użytkownik kliknie działanie przeglądarki. W tym celu wyszukuje na stronie element iframe i wywołuje na nim funkcję postMessage() 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:
browser.action.onClicked.addListener(() => {
browser.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, '*');
});
robić coś niebezpiecznego,
Gdy wczytany zostanie plik sandbox.html, wczytana zostanie biblioteka Handlebars, a następnie zostanie utworzony i skompilowany szablon wbudowany w sposób sugerowany przez 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>
To się nie może nie udać! Mimo że Handlebars.compile ostatecznie używa new Function, wszystko działa zgodnie z oczekiwaniami i w templates['hello'] otrzymujemy skompilowany szablon.
Przekazywanie wyniku
Udostępnimy ten szablon, konfigurując odbiornik wiadomości, który akceptuje polecenia ze strony rozszerzenia. Użyjemy przekazanego parametru command, aby określić, co należy zrobić (możesz sobie wyobrazić, że robisz coś więcej niż tylko renderowanie, np. tworzenie szablonów). być może zarządzanie nimi w jakiś sposób), a wartość context zostanie przekazana bezpośrednio do szablonu w celu renderowania. Wyrenderowany kod HTML
zostanie przekazany z powrotem na stronę rozszerzenia, aby rozszerzenie mogło go później wykorzystać:
<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>
Na stronie rozszerzenia pojawi się ten komunikat, a my wykonamy ciekawe działanie z przekazanymi html
danymi. W tym przypadku wyświetlimy go w powiadomieniu, ale można bezpiecznie używać tego kodu HTML w interfejsie rozszerzenia. Wstawianie go za pomocą elementu innerHTML nie stanowi poważnego zagrożenia dla bezpieczeństwa, ponieważ ufamy treściom, które zostały wyrenderowane w piaskownicy.
Ten mechanizm ułatwia tworzenie szablonów, ale nie ogranicza się tylko do tego. Każdy kod, który nie działa od razu w ramach ścisłej (standard) Content Security Policy, można umieścić w piaskownicy. W rzeczywistości często przydatne jest umieszczanie w piaskownicy komponentów rozszerzeń, które działałyby prawidłowo, aby ograniczyć każdą część programu do najmniejszego zestawu uprawnień niezbędnych do prawidłowego wykonania. Prezentacja Writing Secure Web Apps and Chrome Extensions z konferencji Google I/O 2012 zawiera kilka dobrych przykładów zastosowania tych technik w praktyce i warto poświęcić na nią 56 minut.