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

System rozszerzeń do Chrome wymusza dość rygorystyczne ustawienia polityki bezpieczeństwa treści (CSP). Ograniczenia w zasadach są proste: skrypt musi zostać przeniesiony do osobnych plików JavaScript, a obsługa zdarzeń w wierszu kodu musi zostać przekonwertowana na obsługę za pomocą funkcji addEventListener. Funkcja eval() jest wyłączona.

Zdajemy sobie jednak sprawę, że wiele bibliotek używa konstrukcji podobnych do eval()eval, takich jak new Function(), w celu optymalizacji wydajności i ułatwienia wyrażania. Biblioteki szablonów szczególnie podatne na ten styl implementacji. Chociaż niektóre z nich (np. Angular.js) obsługują CSP, wiele popularnych frameworków nie zostało jeszcze zaktualizowanych do mechanizmu zgodnego z modelem bez rozszerzeń. Usunięcie obsługi tej funkcji okazało się więc bardziej dowodem niż oczekiwano.

Ten dokument przedstawia piaskownicę jako bezpieczny mechanizm umieszczania tych bibliotek w projektach. bez wpływu na bezpieczeństwo.

Dlaczego piaskownica?

eval jest niebezpieczny w rozszerzeniu, ponieważ kod, który wykonuje, ma dostęp do wszystkiego w środowisku o wysokich uprawnieniach rozszerzenia. Dostępnych jest wiele zaawansowanych interfejsów API chrome.*, które pozwalają poważnie wpływać na bezpieczeństwo i prywatność użytkowników; prosty wydobycie danych to dla nas najmniejszy problem. Oferowane rozwiązanie to piaskownica, w której eval może wykonywać kod bez dostępu do danych rozszerzenia ani do interfejsów API o wysokiej wartości. Brak danych, brak interfejsów API, brak problemów.

Można to osiągnąć, umieszczając określone pliki HTML w pakiecie rozszerzeń jako środowisko umieszczone w piaskownicy. Podczas wczytywania strony w piaskownicy zostanie ona przeniesiona do wyjątkowego źródła i nie będzie mieć dostępu do interfejsów API chrome.*. Jeśli wczytamy tę stronę w piaskownicy do naszego rozszerzenia za pomocą interfejsu iframe, i zaczekaj, aż przekaże nam wiadomość, wynik. Ten prosty mechanizm przesyłania wiadomości daje nam wszystko, czego potrzebujemy, aby bezpiecznie umieszczać reklamy oparte na eval w przepływie pracy naszego rozszerzenia.

Tworzenie i używanie piaskownicy

Jeśli chcesz zacząć od kodowania, pobierz przykładowe rozszerzenie z sandboxem i spróbuj. To działający przykład interfejsu API do wysyłania wiadomości, który został stworzony na podstawie biblioteki szablonów Handlebars. Powinien on zawierać wszystko, czego potrzebujesz na początek. Dla tych, którzy omówimy dokładniej tę próbkę.

Wyświetl listę plików w manifeście

Każdy plik, który powinien zostać uruchomiony w środowisku piaskownicy, musi być wymieniony w manifeście rozszerzenia przez dodanie parametru sandbox. To kluczowy krok, o którym łatwo zapomnieć, dlatego sprawdź, czy plik z 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 trybie piaskownicy

Aby zrobić coś interesującego z plikiem w trybie piaskownicy, musimy wczytać go w kontekście, można go zaadresować za pomocą kodu rozszerzenia. W tym przypadku plik sandbox.html został załadowany na stronę rozszerzenia za pomocą iframe. Plik JavaScript strony zawiera kod, który wysyła wiadomość do piaskownicy po każdym kliknięciu działania przeglądarki przez znalezienie iframe na stronie i wywołanie postMessage() na jego contentWindow. Wiadomość to obiekt zawierający 3 właściwości: context, templateNamecommand. Za chwilę przyjrzymy się bliżej 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, '*');
  });

robisz coś niebezpiecznego;

Po załadowaniu interfejsu sandbox.html wczytuje bibliotekę obsługi, a następnie tworzy i kompiluje link w tekście w taki sposób, w jaki sugeruje to aplikacja 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 nie problem! Mimo że Handlebars.compile korzysta z usługi new Function, wszystko działa zgodnie z oczekiwaniami – otrzymujemy skompilowany szablon w templates['hello'].

Przekazywanie wyniku z powrotem

Szablon ten będzie dostępny do użycia przez skonfigurowanie detektora wiadomości, który będzie akceptować polecenia na stronie rozszerzeń. Użyjemy przekazanej wartości command, aby określić, co należy zrobić (możesz wyobrazić sobie coś więcej niż renderowanie, np. tworzenie szablonów). Być może w niektórych przypadkach ?), a elementy context będą przekazywane bezpośrednio do szablonu w celu renderowania. Wyrenderowany HTML zostanie zwrócony do strony rozszerzenia, aby rozszerzenie mogło później wykonać z nim coś przydatnego:

 <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ść, aby wykorzystać w usłudze html coś interesującego przekazywane nam dane. W tym przypadku powiadomimy Cię o tym w powiadomieniu, ale pozwala na bezpieczne użycie tego kodu HTML jako części interfejsu rozszerzenia. Wstawianie ich za pomocą innerHTML nie stwarza poważnego zagrożenia dla bezpieczeństwa, ponieważ ufamy treściom wyrenderowanym w piaskownicy.

Ten mechanizm upraszcza tworzenie szablonów, ale oczywiście nie ogranicza się tylko do nich. Do piaskownicy można umieścić dowolny kod, który nie działa w ramach rygorystycznej polityki bezpieczeństwa treści. Często warto umieścić w piaskownicy komponenty rozszerzeń, które działałyby prawidłowo, aby ograniczyć każdemu elementowi programu najmniejszy zestaw uprawnień niezbędnych do jego prawidłowego działania. Prezentacja Writing Secure Web Apps and Chrome Extensions z Google I/O 2012 zawiera dobre przykłady zastosowania tych technik i warto poświęcić na nią 56 minut.