Korzystanie z eval w rozszerzeniach do Chrome

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. W aplikacjach Chrome są stosowane jeszcze bardziej rygorystyczne zasady i jesteśmy zadowoleni z właściwości zabezpieczeń, jakie zapewniają.

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. W związku z tym będziemy używać terminu rozszerzenia, ale odnosi się on w równym stopniu do aplikacji.

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 do 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 stronie zdarzeń rozszerzenia (eventpage.html) przez tag iframe. Plik eventpage.js zawiera kod, który po każdym kliknięciu działania przeglądarki wysyła komunikat do piaskownicy. Aby to zrobić, znajduje obiekt iframe na stronie i uruchamia metodę postMessage w elemencie contentWindow. Wiadomość jest obiektem zawierającym 2 właściwości: context i command. Za chwilę omówimy obydwa z nich.

chrome.browserAction.onClicked.addListener(function() {
 var iframe = document.getElementById('theFrame');
 var message = {
   command: 'render',
   context: {thing: 'world'}
 };
 iframe.contentWindow.postMessage(message, '*');
});
Ogólne informacje o interfejsie postMessage API znajdziesz w dokumentacji postMessage w MDN . To kompletna treść, którą warto przeczytać. Pamiętaj, że dane można przekazywać w tę i z powrotem tylko wtedy, gdy nadają się do serializacji. Funkcje nie są na przykład dostępne.

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:

<script src="handlebars-1.0.0.beta.6.js"></script>
<script id="hello-world-template" type="text/x-handlebars-template">
  <div class="entry">
    <h1>Hello, !</h1>
  </div>
</script>
<script>
  var templates = [];
  var source = document.getElementById('hello-world-template').innerHTML;
  templates['hello'] = Handlebars.compile(source);
</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 zdarzenia. 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ę zdarzenia, dzięki czemu rozszerzenie będzie mogło później robić z nim coś przydatnego:

<script>
  window.addEventListener('message', function(event) {
    var command = event.data.command;
    var name = event.data.name || 'hello';
    switch(command) {
      case 'render':
        event.source.postMessage({
          name: name,
          html: templates[name](event.data.context)
        }, event.origin);
        break;

      // case 'somethingElse':
      //   ...
    }
  });
</script>

Po powrocie na stronę wydarzenia otrzymamy tę wiadomość i zrobimy coś ciekawego z przekazanymi danymi html. W tym przypadku odczytamy go w powiadomieniu na pulpicie, ale istnieje możliwość, że bezpiecznie użyjemy tego kodu HTML w interfejsie rozszerzenia. Wstawienie go za pomocą innerHTML nie stwarza poważnego zagrożenia dla bezpieczeństwa, ponieważ nawet całkowite przejęcie jego kodu w piaskownicy poprzez sprytny atak nie pozwoliłoby wstrzyknąć niebezpiecznej zawartości skryptu lub wtyczki do kontekstu rozszerzenia o wysokich uprawnieniach.

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.