Graj bezpiecznie w elementach iframe w trybie piaskownicy

Tworzenie zaawansowanych treści w internecie niemal nieuniknionego wiąże się z umieszczaniem komponentów i treści, nad którymi nie masz żadnej kontroli. Widżety innych firm mogą zwiększać zaangażowanie i odgrywać kluczową rolę w ogólnym procesie korzystania z witryny. Treści użytkowników są czasem nawet ważniejsze niż natywna treść witryny. Powstrzymanie się od któregoś z tych sposobów nie jest dobrym rozwiązaniem, ale zwiększa ryzyko, że w witrynie stanie się coś złego. Każdy widżet (reklama i widżet mediów społecznościowych) może stać się wektorem ataku dla osób z niepożądanymi intencjami:

Polityka bezpieczeństwa treści (CSP) może zniwelować ryzyko związane z tymi typami treści, dając Ci możliwość umieszczania na białej liście specjalnie zaufanych źródeł skryptów i innych treści. To ważny krok we właściwym kierunku, ale warto zauważyć, że zabezpieczenia, jakie zapewnia większość dyrektyw CSP, mają charakter binarny – zasób jest dozwolony lub nie. Czasem może być przydatne powiedzenie „Nie wiem, czy ufam temu źródłom treści, ale jest ono bardzo ładne. Umieść ją w przeglądarce, ale nie pozwól, żeby zepsuła witrynę”.

Najniższy poziom uprawnień

W skrócie szukamy mechanizmu, który pozwoli nam udostępniać treści, które mają minimalny poziom uprawnień niezbędnych do wykonania zadania. Jeśli widżet nie musi pojawiać się w nowym oknie, odebranie możliwości window.open nie będzie miało sensu. Jeśli wtyczka nie wymaga Flasha, wyłączenie obsługi wtyczki nie powinno być problemem. Jesteśmy w maksymalnym bezpieczeństwie, jeśli przestrzegamy zasady jak najmniejszych uprawnień i blokujemy każdą funkcję, która nie jest bezpośrednio związana z funkcjami, których chcemy używać. W rezultacie nie musimy już ślepo wierzyć, że pewne umieszczone treści nie będą korzystać z przywilejów, z których nie powinno się korzystać. Po prostu nie będą mieć dostępu do funkcji.

Elementy iframe stanowią pierwszy krok w kierunku dobrej struktury takiego rozwiązania. Wczytywanie niezaufanego komponentu w iframe pozwala określić odległość między aplikacją a treścią, którą chcesz załadować. Treści w ramce nie będą miały dostępu do modelu DOM strony ani danych przechowywanych lokalnie i nie będą mogły wyświetlać się na dowolnych pozycjach na stronie. Ich zakres jest ograniczony do zarysu ramki. Jednak podział nie jest tak wyraźny. Zawarta strona ma wiele opcji irytujących lub złośliwych zachowań: autoodtwarzanie filmów, wtyczki i wyskakujące okienka to wierzchołek góry lodowej.

Atrybut sandbox elementu iframe przekazuje nam to, czego potrzebujemy, aby ograniczyć ograniczenia dotyczące treści w ramkach. Możemy polecić przeglądarce wczytanie zawartości określonej klatki w środowisku o niskich uprawnieniach, udostępniając jedynie ten podzbiór funkcji, które są niezbędne do działania.

Zmodyfikuj, ale sprawdź

Przycisk „tweet” na Twitterze to świetny przykład funkcji, którą można bezpieczniej umieścić w witrynie za pomocą piaskownicy. Twitter umożliwia umieszczenie przycisku w elemencie iframe za pomocą tego kodu:

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

Aby określić, co możemy zablokować, sprawdźmy dokładnie, jakich funkcji wymaga ten przycisk. Kod HTML wczytywany w ramce wykonuje fragment kodu JavaScript z serwerów Twittera i po kliknięciu generuje wyskakujące okienko z interfejsem tweetowania. Ten interfejs musi mieć dostęp do plików cookie Twittera, aby powiązać tweeta z właściwym kontem i mieć możliwość przesłania formularza. To w zasadzie wszystko. Ramka nie wymaga ładowania żadnych wtyczek, nie musi przechodzić przez okno najwyższego poziomu ani korzystać z innych funkcji. Nie są potrzebne te uprawnienia, więc usuńmy je, dzieląc zawartość ramki na piaskownice.

Piaskownica działa na podstawie białej listy. Zaczniemy od usunięcia wszystkich możliwych uprawnień, a następnie ponownie włączymy poszczególne funkcje, dodając określone flagi do konfiguracji piaskownicy. Zdecydowaliśmy się włączyć obsługę JavaScriptu, wyskakujących okienek, przesyłania formularzy i plików cookie na twitter.com Możemy to zrobić, dodając do iframe atrybut sandbox o takiej wartości:

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
    src="https://platform.twitter.com/widgets/tweet_button.html"
    style="border: 0; width:130px; height:20px;"></iframe>

To wszystko. Udostępniliśmy ramce wszystkie wymagane możliwości i przeglądarka zablokuje mu dostęp do uprawnień, których nie przyznaliśmy jednoznacznie za pomocą wartości atrybutu sandbox.

Szczegółowa kontrola nad możliwościami

W przykładzie powyżej przedstawiliśmy kilka możliwych flag piaskownicy. Teraz bardziej szczegółowo przyjrzyjmy się jego działaniu.

Jeśli element iframe będzie miał pusty atrybut piaskownicy, dokument w ramce będzie w pełni umieszczony w piaskownicy i będzie podlegać tym ograniczeniom:

  • Kod JavaScript nie zostanie wykonany w dokumencie w ramce. Dotyczy to nie tylko kodu JavaScript ładowanego bezpośrednio przez tagi skryptu, ale też wbudowanych modułów obsługi zdarzeń oraz adresów URL javascript:. Oznacza to również, że treści zawarte w tagach noscript będą wyświetlane dokładnie tak, jak gdyby użytkownik sam wyłączył skrypt.
  • Dokument w ramce jest wczytywany do unikalnego źródła, co oznacza, że wszystkie kontrole tego samego pochodzenia zakończą się niepowodzeniem. Unikalne źródła nigdy nie pasują do żadnych innych źródeł ani nawet do siebie. Oznacza to między innymi, że dokument nie ma dostępu do danych przechowywanych w plikach cookie źródła ani w innych mechanizmach przechowywania danych (pamięć DOM, Indexed DB itp.).
  • Dokument w ramce nie może tworzyć nowych okien ani okien (na przykład za pomocą window.open czy target="_blank").
  • Nie można przesłać formularzy.
  • Wtyczki się nie wczytają.
  • Dokument w ramce może poruszać się tylko sam siebie, a nie do dokumentu nadrzędnego najwyższego poziomu. Ustawienie window.top.location spowoduje zgłoszenie wyjątku, a kliknięcie linku zawierającego wartość target="_top" nie przyniesie żadnego efektu.
  • Funkcje uruchamiane automatycznie (automatyczne elementy formularzy, autoodtwarzanie filmów itp.) są blokowane.
  • Nie udało się uzyskać blokady wskaźnika.
  • Atrybut seamless jest ignorowany w elemencie iframes zawartym w dokumencie w ramce.

Jest to dość drakońskie, a dokument wczytany do środowiska iframe w pełni piaskownicy nie stwarza realnego ryzyka. Oczywiście nie przyniesie to zbyt wiele korzyści. Niektóre treści statyczne możesz umieścić w pełnej piaskownicy, ale w większości przypadków lepiej będzie to nieco rozluźnić.

Z wyjątkiem wtyczek każde z tych ograniczeń można znieść, dodając flagę do wartości atrybutu sandbox. Dokumenty w trybie piaskownicy nigdy nie uruchamiają wtyczek, ponieważ są to ich natywny kod, ale wszystko inne jest uczciwe:

  • allow-forms umożliwia przesłanie formularza.
  • allow-popups zezwala na wyskakujące okienka (szok!).
  • allow-pointer-lock zezwala (niespodzianka!) na blokadę wskaźnika.
  • allow-same-origin pozwala dokumentowi zachować swoje pochodzenie. Strony wczytane z domeny https://example.com/ zachowają dostęp do danych z tego źródła.
  • allow-scripts umożliwia wykonywanie kodu JavaScript oraz umożliwia automatyczne uruchamianie funkcji (ponieważ ich implementacja za pomocą JavaScriptu jest prosta).
  • allow-top-navigation umożliwia dokumentowi wyjście poza ramkę przez nawigowanie w oknie najwyższego poziomu.

Mając to na uwadze, możemy dokładnie ocenić, dlaczego w powyższym przykładzie z Twitterem wykorzystujemy konkretny zestaw flag piaskownicy:

  • Parametr allow-scripts jest wymagany, ponieważ strona wczytana w ramce uruchamia JavaScript do obsługi interakcji użytkownika.
  • Parametr allow-popups jest wymagany, ponieważ po kliknięciu przycisku pojawia się formularz tweeta w nowym oknie.
  • Wymagany jest allow-forms – formularz do tweetowania powinien być formularzem przesyłania.
  • Adres allow-same-origin jest niezbędny, ponieważ pliki cookie ze strony twitter.com byłyby niedostępne, a użytkownik nie mógł się zalogować, aby opublikować formularz.

Ważne jest, że flagi piaskownicy zastosowane do ramki są też stosowane do wszystkich okien i ramek utworzonych w piaskownicy. Oznacza to, że musimy dodać allow-forms do piaskownicy ramki, mimo że formularz istnieje tylko w oknie, w którym pojawi się ramka.

Po zastosowaniu atrybutu sandbox widżet otrzymuje tylko wymagane uprawnienia, a możliwości, takie jak wtyczki, górna nawigacja i blokada wskaźnika, pozostają zablokowane. Zmniejszyliśmy ryzyko umieszczenia widżetu na stronie, co nie powoduje żadnych niepożądanych efektów. To korzyści dla wszystkich.

Rozdzielenie uprawnień

Umieszczanie treści osób trzecich w piaskownicy w celu uruchamiania ich niezaufanego kodu w środowisku o niskich uprawnieniach jest całkiem oczywiste. A co z własnym kodem? Możesz sobie zaufać, prawda? Po co więc martwić się o tryb piaskownicy?

Odwrócę pytanie: po co dawać dostęp do wtyczek, jeśli Twój kod nie wymaga wtyczek? Jest przywilejem, którego nigdy nie wykorzystujesz, a najgorszym, że może on pomóc hakerom wkroczyć do środka. Każdy kod zawiera błędy i prawie każda aplikacja jest w jakiś sposób podatna na nadużycia. Umieszczanie własnego kodu w piaskownicy oznacza, że nawet jeśli atakujący zdoła ją zablokować, nie otrzyma pełnego dostępu do źródła aplikacji. Będzie mógł robić tylko rzeczy, które aplikacja może robić. Jest to sytuacja niekorzystna, ale nie tak zła, jak by mogła.

Aby jeszcze bardziej zmniejszyć ryzyko, możesz podzielić aplikację na logiczne fragmenty i umieścić je w piaskownicy przy minimalnych możliwych uprawnieniach. Ta technika jest bardzo rozpowszechniona w kodzie natywnym: na przykład Chrome włamuje się do procesu przeglądarki o wysokich uprawnieniach, który ma dostęp do lokalnego dysku twardego i może nawiązać połączenia sieciowe, oraz z wielu procesów renderowania o niskich uprawnieniach, które wykonują ciężką pracę związaną z analizą niezaufanych treści. Mechanizmy renderowania nie muszą dotykać dysku – to przeglądarka przekazuje wszystkie informacje potrzebne do wyrenderowania strony. Nawet jeśli sprytny haker znajdzie sposób na uszkodzenie mechanizmu renderowania, nie jest daleko, bo ten mechanizm sam nie potrafi wiele zmienić. Dostęp o wysokich uprawnieniach musi być przekierowany przez cały proces przeglądarki. Aby wyrządzić ewentualne szkody, osoby przeprowadzające atak będą musiały znaleźć kilka otworów w różnych częściach systemu, co znacznie zmniejsza ryzyko udanego ataku.

Bezpieczne korzystanie z piaskownicy w usłudze eval()

Dzięki piaskownicy i interfejsowi API postMessage sukces tego modelu można dość łatwo zastosować w internecie. Fragmenty aplikacji mogą znajdować się w piaskownicach iframe, a dokument nadrzędny może pośredniczyć w komunikacji między nimi, publikując wiadomości i sprawdzając odpowiedzi. Taki rodzaj struktury powoduje, że luki w zabezpieczeniach w żadnym elemencie aplikacji wyrządzają minimalne możliwe szkody. Ma też tę zaletę, że trzeba tworzyć jasno określone punkty integracji, dzięki czemu dokładnie wiesz, gdzie trzeba uważać przy weryfikowaniu danych wejściowych i wyjściowych. Przyjrzyjmy się przykładowi zabawki, aby zobaczyć, jak może ona działać.

Evalbox to ekscytująca aplikacja, w której ciąg tekstowy jest oceniany jako JavaScript. Niesamowite, prawda? Po prostu na to, na co czekaliście przez te długie lata. Jest to oczywiście dość niebezpieczna aplikacja, ponieważ zezwolenie na wykonanie dowolnego JavaScriptu sprawia, że masz dostęp do wszystkich danych, które oferuje źródło. Będziemy ograniczać ryzyko wystąpienia Bad ThingsTM, upewniając się, że kod jest uruchamiany wewnątrz piaskownicy, co sprawia, że jest to nieco bezpieczniejsze. Kod przetworzymy od wewnątrz, zaczynając od zawartości ramki:

<!-- frame.html -->
<!DOCTYPE html>
<html>
    <head>
    <title>Evalbox's Frame</title>
    <script>
        window.addEventListener('message', function (e) {
        var mainWindow = e.source;
        var result = '';
        try {
            result = eval(e.data);
        } catch (e) {
            result = 'eval() threw an exception.';
        }
        mainWindow.postMessage(result, event.origin);
        });
    </script>
    </head>
</html>

W ramce znajduje się niewielki dokument, który po prostu nasłuchuje wiadomości od swojego elementu nadrzędnego, łącząc się ze zdarzeniem message obiektu window. Za każdym razem, gdy element nadrzędny uruchamia postMessage w elemencie iframe, to zdarzenie jest wywoływane, dając nam dostęp do ciągu, który ma wykonać nasz element nadrzędny.

W module obsługi bierzemy atrybut source zdarzenia, który jest oknem nadrzędnym. Wykorzystamy je, by przesłać wyniki naszej ciężkiej pracy, gdy już skończymy. Potem wykonujemy całą pracę związaną z najtrudniejszymi zadaniami, przesyłając dane, które otrzymaliśmy do usługi eval(). To wywołanie zostało zakończone w bloku testowania, ponieważ zablokowane operacje w obiekcie iframe w trybie piaskownicy często generują wyjątki DOM. Wykryjemy je i zgłosimy przyjazny komunikat o błędzie. Na koniec publikujemy wynik w oknie nadrzędnym. To całkiem proste.

Proces nadrzędny nie jest skomplikowany. Utworzymy niewielki interfejs użytkownika z textarea na potrzeby kodu i z button do wykonania. Pobierzemy frame.html przez iframe w trybie piaskownicy, co pozwoli tylko na wykonywanie skryptu:

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

Teraz przekażemy sprawę do wykonania. Po pierwsze, słuchamy odpowiedzi użytkowników iframe i alert(). Pewnie prawdziwa aplikacja byłaby mniej irytująca:

window.addEventListener('message',
    function (e) {
        // Sandboxed iframes which lack the 'allow-same-origin'
        // header have "null" rather than a valid origin. This means you still
        // have to be careful about accepting data via the messaging API you
        // create. Check that source, and validate those inputs!
        var frame = document.getElementById('sandboxed');
        if (e.origin === "null" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

Następnie podłączmy moduł obsługi zdarzeń do kliknięć linku button. Gdy użytkownik kliknie plik, pobierze bieżącą zawartość pliku textarea i przekaże ją do ramki do wykonania:

function evaluate() {
    var frame = document.getElementById('sandboxed');
    var code = document.getElementById('code').value;
    // Note that we're sending the message to "*", rather than some specific
    // origin. Sandboxed iframes which lack the 'allow-same-origin' header
    // don't have an origin which you can target: you'll have to send to any
    // origin, which might alow some esoteric attacks. Validate your output!
    frame.contentWindow.postMessage(code, '*');
}

document.getElementById('safe').addEventListener('click', evaluate);

Łatwe, prawda? Opracowaliśmy bardzo prosty interfejs API do oceny i mamy pewność, że oceniany kod nie ma dostępu do informacji poufnych, takich jak pliki cookie czy pamięć DOM. Analogicznie oceniany kod nie może wczytywać wtyczek, otwierać nowych okien ani wykonywać innych irytujących lub złośliwych działań.

To samo możesz zrobić z własnym kodem, dzieląc aplikacje monolityczne na komponenty jednozadaniowe. Każdy z nich można umieścić w prostym interfejsie API do przesyłania wiadomości, tak jak omówiliśmy powyżej. Okno nadrzędne o wysokich uprawnieniach może pełnić rolę kontrolera i dyspozytora, wysyłając wiadomości do określonych modułów, z których każdy ma jak najmniejsze uprawnienia do wykonywania swoich zadań, nasłuchiwać wyników i pilnować, aby każdy moduł był dobrze wyposażony w tylko niezbędne informacje.

Pamiętaj jednak, aby zachować ostrożność w przypadku treści w ramkach, które pochodzą z tego samego pochodzenia co plik nadrzędny. Jeśli strona w witrynie https://example.com/ umieszcza w ramce innej strony w tym samym źródle treści w piaskownicy, która zawiera zarówno flagi allow-same-origin, jak i allow-scripts, strona z ramką może dotrzeć do elementu nadrzędnego i całkowicie usunąć atrybut sandbox.

Graj w trybie piaskownicy

Piaskownica jest obecnie dostępna w różnych przeglądarkach: Firefox 17 i nowszych, IE10+ oraz Chrome w momencie tworzenia tekstu (w wersji Caniuse znajdziesz oczywiście aktualną tabelę pomocy). Zastosowanie atrybutu sandbox do uwzględnianych elementów iframes pozwala przyznać określone uprawnienia do wyświetlanych treści tylko w przypadku tych uprawnień, które są niezbędne do prawidłowego działania treści. Daje to możliwość zmniejszenia ryzyka związanego z uwzględnieniem treści osób trzecich w sposób wykraczający poza możliwości oferowane przez politykę bezpieczeństwa treści.

Co więcej, piaskownica to skuteczna technika ograniczania ryzyka, że sprytny haker będzie w stanie wykorzystać luki w kodzie w Twoim kodzie. Po podzieleniu aplikacji monolitycznej na zestaw usług w piaskownicy, z których każda odpowiada za niewielki fragment autonomicznych funkcji, osoby przeprowadzające atak będą musiały przejąć kontrolę nie tylko do zawartości konkretnych ramek, ale też do swojego kontrolera. To znacznie trudniejsze zadanie, zwłaszcza że zakres działania kontrolera można znacznie ograniczyć. Możesz poświęcić na kontrolę tego kodu, jeśli poprosisz przeglądarkę o pomoc z resztą.

Nie oznacza to, że tryb piaskownicy to kompletne rozwiązanie problemu z bezpieczeństwem w internecie. Zapewnia on dogłębną ochronę i bez kontroli nad klientami swoich użytkowników nie można jeszcze polegać na obsłudze przeglądarek przez wszystkich użytkowników (jeśli kontrolujesz klientów należących do użytkowników – na przykład środowisko firmowe – hura!). Kiedyś... ale na razie piaskownica to dodatkowa warstwa zabezpieczeń, która wzmocni Twoją obronę. Nie jest to kompletna ochrona, na której możesz polegać. Mimo to warstwy są świetne. Zalecamy skorzystanie z tego przykładu.

Dalsza lektura

  • Rozdzielenie uprawnień w aplikacjach HTML5” to ciekawy artykuł na temat projektowania niewielkiej platformy i zastosowania jej w 3 istniejących aplikacjach HTML5.

  • Piaskownica może być jeszcze bardziej elastyczna w połączeniu z 2 innymi nowymi atrybutami elementów iframe: srcdoc i seamless. Pierwsza opcja umożliwia zapełnienie ramki zawartością ramki bez obciążenia żądania HTTP. Drugie umożliwia wykorzystywanie stylu do treści w ramce. Oba typy przeglądarek są obecnie dosyć słabo obsługiwane (np. wiadomości nocne z Chrome i WebKit), ale w przyszłości ich połączenie będzie interesujące. Możesz na przykład umieścić w trybie piaskownicy komentarze do artykułu, korzystając z tego kodu:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>