Używanie eval() w elementach iframe umieszczonych w piaskownicy

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 procedury obsługi zdarzeń muszą zostać przekonwertowane na addEventListener, a eval() jest wyłączony.

Zdajemy sobie jednak sprawę, że różne biblioteki używają konstrukcji podobnych do eval()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 piaskownica?

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 chrome.* interfejsów API, które mogą poważnie wpłynąć na bezpieczeństwo i prywatność użytkownika. Proste wydobycie 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 interfejsów API o wysokiej wartości. Brak danych, brak interfejsów API, brak problemów.

Osiągamy to, umieszczając w pakiecie rozszerzenia listę konkretnych plików HTML, które mają być uruchamiane w piaskownicy. Za każdym razem, gdy załadowana zostanie strona w piaskownicy, zostanie ona przeniesiona do unikalnego pochodzenia i nie będzie miała dostępu do interfejsów chrome.* API. Jeśli załadujemy tę stronę w piaskownicy do rozszerzenia za pomocą elementu iframe, możemy przekazywać jej wiadomości, pozwalać jej na wykonywanie na nich określonych działań i czekać, aż zwróci 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 i używanie piaskownicy

Jeśli chcesz od razu przejść do kodu, pobierz przykładowe rozszerzenie do testowania w środowisku 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 bardzo ważny krok, o którym łatwo zapomnieć, więc sprawdź, czy plik w piaskownicy jest wymieniony w pliku manifestu. W tym przykładzie umieszczamy w piaskownicy plik o nazwie „sandbox.html”. Wpis w pliku manifestu wygląda tak:

{
  ...,
  "sandbox": {
     "pages": ["sandbox.html"]
  },
  ...
}

Wczytywanie pliku w piaskownicy

Aby można było zrobić coś ciekawego z plikiem w piaskownicy, musimy go załadować 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 wiadomość 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 funkcję postMessage() na jego obiekcie contentWindow. Wiadomość jest obiektem zawierającym 3 właściwości: context, templateNamecommand. Za chwilę omówimy contextcommand.

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, '*');
  });

wykonywać niebezpieczne czynności,

Gdy wczytany zostanie plik sandbox.html, wczytana zostanie biblioteka Handlebars, a następnie utworzony i skompilowany zostanie 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 detektor 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). Może to być zarządzanie nimi w określony 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 po prostu wyświetlimy go w powiadomieniu, ale można bezpiecznie używać tego kodu HTML w interfejsie rozszerzenia. Wstawianie go za pomocą 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 oczywiście 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 i warto poświęcić na nią 56 minut.