Graj bezpiecznie w elementach iframe w trybie piaskownicy

Stworzenie bogatego środowiska w dzisiejszej sieci prawie zawsze wymaga umieszczania komponentów i treści, nad którymi nie masz realnej kontroli. Widżety innych firm mogą zwiększać zaangażowanie i odgrywać kluczową rolę w ogólnym wrażeniu użytkowników, a treści użytkowników są czasem nawet ważniejsze niż treści natywne strony. Nie możesz zrezygnować z żadnego z tych rozwiązań, ale oba zwiększają ryzyko, że w Twojej witrynie może się wydarzyć coś złego. Każdy wbudowany widżet, każda reklama, każdy widżet mediów społecznościowych to potencjalny wektor ataku dla osób o złośliwych zamiarach:

Polityka bezpieczeństwa treści (CSP) może ograniczyć ryzyko związane z obu tymi typami treści, ponieważ umożliwia dodanie do białej listy zaufanych źródeł skryptów i innych treści. To duży krok w odpowiednią stronę, ale warto pamiętać, że ochrona zapewniana przez większość dyrektyw CSP jest binarna: zasób jest dozwolony lub niedozwolony. Czasami warto powiedzieć: „Nie wiem, czy ufać temu źródłu treści, ale jest ono bardzo ładne. Umieść to w swojej witrynie, przeglądarko, ale nie rób tego w sposób, który spowoduje jej uszkodzenie”.

Zasada jak najmniejszych uprawnień

Szukamy mechanizmu, który pozwoli nam przyznać treściom, które osadzamy, tylko minimalny poziom uprawnień niezbędnych do wykonania zadania. Jeśli widżet nie musi otwierać nowego okna, odebranie dostępu do window.open nie powinno mieć negatywnego wpływu. Jeśli nie wymaga Flasha, wyłączenie obsługi wtyczek nie powinno stanowić problemu. Stosujemy zasadę minimalnego poziomu uprawnień i blokujemy wszystkie funkcje, które nie są bezpośrednio związane z funkcjonalnością, której chcemy użyć. Dzięki temu nie musimy już ślepo ufać, że treści osadzone nie będą korzystać z uprawnień, których nie powinny. po prostu nie będzie mieć dostępu do tej funkcji.

iframe to pierwszy krok w kierunku stworzenia odpowiedniej struktury dla takiego rozwiązania. Wczytanie niezaufanego komponentu w ramach iframe zapewnia pewien stopień oddzielenia aplikacji od treści, które chcesz wczytać. Treści w ramce nie będą miały dostępu do DOM strony ani danych przechowywanych lokalnie. Nie będą też mogły rysować w dowolnych miejscach na stronie. Ich zakres jest ograniczony do obrysowania ramki. Podział nie jest jednak w pełni niezawodny. Zawarty na niej element nadal może powodować irytujące lub szkodliwe działanie. Automatycznie odtwarzane filmy, wtyczki i wyskakujące okienka to tylko wierzchołek góry lodowej.

Atrybut sandbox elementu iframepozwala nam zawęzić ograniczenia dotyczące treści w ramce. Możemy poprosić przeglądarkę o wczytanie treści określonego elementu w środowisku o ograniczonych uprawnieniach, zezwalając tylko na podzbiór funkcji niezbędnych do wykonania danego zadania.

Zaufaj, ale sprawdź

Przycisk „Tweet” w Twitterze to świetny przykład funkcji, którą można bezpiecznie osadzić w witrynie za pomocą piaskownicy. Twitter umożliwia osadzanie przycisku za pomocą elementu iframe za pomocą tego kodu:

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

Aby ustalić, co możemy zablokować, dokładnie przyjrzyjmy się możliwościom, których wymaga przycisk. Kod HTML wczytany do ramki wykonuje odrobinę kodu JavaScript z serwerów Twittera i po kliknięciu generuje wyskakujące okienko z interfejsem do tweetowania. Ten interfejs musi mieć dostęp do plików cookie Twittera, aby powiązać tweeta z odpowiednim kontem, oraz możliwość przesyłania formularza tweetowania. To w zasadzie wszystko. Ramka nie musi wczytywać żadnych wtyczek, nie musi nawigować w oknie najwyższego poziomu ani korzystać z innych funkcji. Ponieważ nie potrzebuje tych uprawnień, usuń je, umieszczając ramkę w piaskownicy.

Sandboksowanie działa na podstawie białej listy. Najpierw usuwamy wszystkie możliwe uprawnienia, a potem włączamy poszczególne funkcje, dodając do konfiguracji piaskownicy odpowiednie flagi. W przypadku widżetu Twittera postanowiliśmy włączyć JavaScript, wyskakujące okienka, przesyłanie formularzy i pliki cookie twitter.com. Możemy to zrobić, dodając do atrybutu sandbox atrybutu iframe o tej 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. Przydzieliliśmy ramce wszystkie wymagane możliwości, a przeglądarka odmówi jej dostępu do wszystkich przywilejów, których nie przyznano jej wprost za pomocą wartości atrybutu sandbox.

Szczegółowa kontrola nad funkcjami

W przykładzie powyżej pokazaliśmy kilka możliwych flag piaskownicy, a teraz przyjrzymy się bliżej temu atrybucie.

Jeśli element iframe ma pusty atrybut piaskownicy, obudowany dokument będzie w pełni umieszczony w piaskownicy, co spowoduje zastosowanie tych ograniczeń:

  • Skrypt JavaScript nie zostanie wykonany w dokumencie w ramce. Dotyczy to nie tylko kodu JavaScript wczytanego za pomocą tagów skryptu, ale też wbudowanych w kodu elementów sterujących zdarzeniami i adresów URL według schematu javascript:. Oznacza to też, że treści zawarte w tagach noscript będą wyświetlane tak, jakby użytkownik sam wyłączył skrypt.
  • Dokument w ramce jest wczytywany do unikalnego źródła, co oznacza, że wszystkie sprawdzania tego samego źródła zakończą się niepowodzeniem. Unikalne źródła nie pasują do żadnych innych źródeł, nawet do siebie. Oznacza to między innymi, że dokument nie ma dostępu do danych przechowywanych w plikach cookie dowolnego pochodzenia ani do żadnych innych mechanizmów przechowywania (np. pamięci DOM, Indexed DB itp.).
  • Dokument w ramce nie może tworzyć nowych okien ani nowych okienek dialogowych (np. za pomocą window.open lub target="_blank").
  • Nie można przesyłać formularzy.
  • Wtyczki się nie wczytują.
  • Dokument w ramce może się poruszać tylko po sobie, a nie po swoim elemencie nadrzędnym najwyższego poziomu. Ustawienie wartości window.top.location spowoduje wyjątek, a kliknięcie linku z wartością target="_top" nie będzie miało żadnego efektu.
  • Funkcje, które uruchamiają się automatycznie (elementy formularzy z automatycznym ustawieniem ostrości, automatycznie odtwarzane filmy itp.), są blokowane.
  • Nie można uzyskać blokady wskaźnika.
  • Atrybut seamless jest ignorowany w przypadku iframes zawartych w dokumencie z ramką.

To bardzo surowe podejście, a dokumenty wczytane do całkowicie odizolowanego środowiska iframestwarzają bardzo niewielkie ryzyko. Oczywiście może to też nie mieć większego znaczenia: w przypadku niektórych treści statycznych możesz stosować pełne środowisko piaskownicy, ale zazwyczaj będziesz musiał nieco poluzować zasady.

Z wyjątkiem wtyczek wszystkie te ograniczenia można ominąć, dodając flagę do wartości atrybutu piaskownicy. Dokumenty w piaskownicy nigdy nie mogą uruchamiać wtyczek, ponieważ są one natywnym kodem w piaskownicy, ale wszystko inne jest dozwolone:

  • allow-forms umożliwia przesłanie formularza.
  • allow-popups zezwala (co może być szokujące) na wyskakujące okienka.
  • allow-pointer-lock umożliwia (co nie jest zaskoczeniem) zablokowanie wskaźnika.
  • allow-same-origin pozwala dokumentowi zachować jego pochodzenie; strony wczytane z https://example.com/ zachowają dostęp do danych tego pochodzenia.
  • allow-scripts umożliwia wykonywanie kodu JavaScriptu, a także automatyczne uruchamianie funkcji (które można łatwo zaimplementować za pomocą JavaScriptu).
  • allow-top-navigation pozwala na wyjście dokumentu poza ramkę przez nawigację w oknie najwyższego poziomu.

Mając to na uwadze, możemy dokładnie określić, dlaczego w przypadku przykładu Twittera powyżej pojawił się określony zestaw flag piaskownicy:

  • allow-scripts jest wymagany, ponieważ strona wczytana do ramki uruchamia kod JavaScript, aby obsłużyć interakcje z użytkownikiem.
  • allow-popups jest wymagane, ponieważ po kliknięciu przycisku w nowym oknie otworzy się formularz tweetowania.
  • allow-forms jest wymagany, ponieważ formularz tweetowania powinien być możliwy do przesłania.
  • allow-same-origin jest konieczne, ponieważ w przeciwnym razie pliki cookie z twitter.com byłyby niedostępne, a użytkownik nie mógłby się zalogować, aby przesłać formularz.

Należy pamiętać, że flagi piaskownicy zastosowane do ramki dotyczą również wszystkich okien lub ramek utworzonych w piaskownicy. Oznacza to, że musimy dodać allow-forms do piaskownicy ramki, mimo że formularz istnieje tylko w oknie, w którym pojawia się ramka.

Dzięki atrybucie sandbox widżet uzyskuje tylko niezbędne uprawnienia, a funkcje takie jak wtyczki, nawigacja górna i blokada wskaźnika pozostają zablokowane. Zmniejszyliśmy ryzyko umieszczania widżetu bez negatywnych skutków. To korzystne dla wszystkich zainteresowanych.

Rozdzielenie uprawnień

Umieszczanie treści innych firm w piaskownicy w celu uruchomienia niesprawdzonego kodu w środowisku o ograniczonych uprawnieniach jest oczywiście korzystne. A co z Twoim własnym kodem? Wierzysz w siebie, prawda? Dlaczego więc martwić się piaskownicą?

Odwrócę pytanie: jeśli Twój kod nie wymaga wtyczek, po co mu dawać dostęp do wtyczek? W najlepszym przypadku jest to przywilej, którego nigdy nie używasz, a w najgorszym – potencjalny wektor, który pozwala atakującym wkraść się do systemu. Każdy kod zawiera błędy, a praktycznie każda aplikacja jest w jakiś sposób podatna na wykorzystanie. Użycie piaskownicy do własnego kodu oznacza, że nawet jeśli atakujący zdoła przechwycić Twoją aplikację, nie będzie miał pełnego dostępu do jej źródła. Będzie mógł wykonywać tylko te czynności, które aplikacja może wykonywać. Nadal jest to złe, ale nie tak złe, jak mogłoby być.

Możesz jeszcze bardziej zmniejszyć ryzyko, dzieląc aplikację na logiczne części i umieszczając każdą z nich w odseparowanym środowisku z minimalnymi uprawnieniami. Ta technika jest bardzo powszechna w kodzie natywnym: na przykład Chrome dzieli się na proces przeglądarki o wysokich uprawnieniach, który ma dostęp do lokalnego dysku twardego i może nawiązywać połączenia sieciowe, oraz wiele procesów mechanizmu renderowania o mniejszych uprawnieniach, które wykonują ciężką pracę związaną z analizowaniem niewiarygodnych treści. Renderery nie muszą dotykać dysku, ponieważ przeglądarka dostarcza im wszystkich informacji potrzebnych do wyrenderowania strony. Nawet jeśli sprytny haker znajdzie sposób na zniszczenie renderowania, nie zdobędzie wiele, ponieważ sam procesor nie może zrobić zbyt wiele: wszystkie uprawnienia o wysokim priorytecie muszą być kierowane przez proces przeglądarki. Aby wyrządzić jakiekolwiek szkody, atakujący muszą znaleźć kilka luk w różnych częściach systemu, co znacznie zmniejsza ryzyko skutecznego przejęcia kontroli nad systemem.

Bezpieczna piaskownica eval()

Dzięki piaskownicy i interfejsowi API postMessage można łatwo zastosować ten model w internecie. Elementy Twojej aplikacji mogą znajdować się w sandboksie iframe, a dokument nadrzędny może pośredniczyć w komunikacji między nimi, wysyłając wiadomości i odbierając odpowiedzi. Taka struktura zapewnia, że luki w poszczególnych częściach aplikacji wyrządzają jak najmniej szkód. Ma to też tę zaletę, że zmusza Cię do tworzenia wyraźnych punktów integracji, dzięki czemu wiesz dokładnie, gdzie musisz zwracać uwagę na sprawdzanie danych wejściowych i wyjściowych. Przyjrzyjmy się temu na przykładzie zabawki.

Evalbox to ciekawa aplikacja, która pobiera ciąg znaków i interpretuje go jako kod JavaScript. Wow, prawda? Dokładnie to, na co czekasz od tylu lat. Jest to oczywiście dość niebezpieczne zastosowanie, ponieważ zezwolenie na wykonywanie dowolnego kodu JavaScript oznacza, że wszystkie dane źródła są dostępne dla potencjalnych hakerów. Zminimalizujemy ryzyko wystąpienia złego scenariusza, zapewniając, aby kod był wykonywany w piaskownicy, co zwiększa bezpieczeństwo. Zaczniemy od treści ramki, a potem przejdziemy do kodu:

<!-- 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>

Wewnątrz ramki znajduje się minimalny dokument, który po prostu nasłuchuje wiadomości od swojego rodzica, korzystając z zdarzenia message obiektu window. Gdy nadrzędny element wykona polecenie postMessage na zawartości elementu iframe, to zdarzenie zostanie wywołane, co da nam dostęp do ciągu znaków, który nadrzędny element chce wykonać.

W obiekcie funkcji pobieramy atrybut source zdarzenia, który jest nadrzędnym oknem. Użyjemy go, aby wysłać Ci wynik naszej ciężkiej pracy, gdy już skończymy. Następnie wykonamy ciężką pracę, przekazując dane do funkcji eval(). To wywołanie zostało umieszczone w bloku try, ponieważ zabronione operacje w piaskownicy iframe często generują wyjątki DOM. Będziemy je przechwytywać i zamiast tego raportować przyjazny komunikat o błędzie. Na koniec publikujemy wynik w oknie nadrzędnym. To dość proste.

Element nadrzędny jest równie prosty. Utworzymy mały interfejs użytkownika z textarea do kodu i button do wykonania. Zaimportujemy frame.html za pomocą piaskownicy iframe, co pozwala na wykonywanie tylko 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 wszystko jest gotowe do wykonania. Najpierw posłuchamy opinii iframe i alert() użytkowników. Prawdopodobnie prawdziwa aplikacja będzie 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 połączymy moduł obsługi zdarzeń z kliknięciami przycisku button. Gdy użytkownik kliknie, pobieramy bieżącą zawartość textarea i przekazujemy ją do wykonania w ramce:

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? Utworzyliśmy bardzo prosty interfejs API do oceny, dzięki któremu możemy mieć pewność, że oceniany kod nie ma dostępu do informacji poufnych, takich jak pliki cookie lub pamięć DOM. Podobnie kod oceniany nie może ładować wtyczek, otwierać nowych okien ani wykonywać żadnych innych irytujących lub szkodliwych działań.

To samo możesz zrobić w przypadku własnego kodu, dzieląc monolityczne aplikacje na komponenty o jednym przeznaczeniu. Każdy z nich można opakować w prosty interfejs Messaging API, tak jak opisaliśmy to powyżej. Okno nadrzędne o wysokich uprawnieniach może pełnić funkcję kontrolera i rozsyłacza, wysyłając wiadomości do określonych modułów, z których każdy ma jak najmniejsze możliwe uprawnienia do wykonywania swoich zadań, słuchając wyników i zapewniając, że każdy moduł jest dobrze zasilany tylko tymi informacjami, których potrzebuje.

Pamiętaj jednak, że musisz zachować szczególną ostrożność podczas korzystania z treści w ramce pochodzących z tego samego źródła co element nadrzędny. Jeśli strona na https://example.com/ zawierającą piaskownicę z flagami allow-same-originallow-scripts, to strona w ramce może dotrzeć do strony nadrzędnej i całkowicie usunąć atrybut piaskownicy.

Granie w piaskownicy

Tryb piaskownicy jest obecnie dostępny w różnych przeglądarkach: Firefox 17 lub nowszym, IE 10 lub nowszym oraz Chrome (w momencie pisania tego tekstu – caniuse, oczywiście, ma aktualną tabelę pomocy). Zastosowanie atrybutu sandbox do uwzględnionego elementu iframes pozwala przyznać wyświetlanym treściom określone uprawnienia, tylko te, które są niezbędne do prawidłowego działania treści. Dzięki temu możesz zmniejszyć ryzyko związane z umieszczaniem treści innych firm, ponad to, co jest już możliwe w ramach standardu Content Security Policy.

Ponadto piaskowanie to skuteczna metoda zmniejszania ryzyka, że sprytny atakujący będzie w stanie wykorzystać luki w Twoim kodzie. Dzięki rozdzieleniu monolitycznej aplikacji na zestaw usług w piaskownicy, z których każda odpowiada za niewielką część samodzielnej funkcji, atakujący będą zmuszeni do skompromitowania nie tylko treści poszczególnych ramek, ale też ich kontrolera. To znacznie trudniejsze zadanie, zwłaszcza że zakres działania kontrolera może być znacznie ograniczony. Jeśli chcesz poświęcić czas na działania związane z bezpieczeństwem, możesz skoncentrować się na tym kodzie, a pozostałe kwestie zostawić przeglądarce.

Nie oznacza to jednak, że piaskownica jest kompletnym rozwiązaniem problemu bezpieczeństwa w internecie. Zapewnia ona wielowarstwową ochronę, ale jeśli nie masz kontroli nad klientami użytkowników, nie możesz jeszcze polegać na obsłudze przeglądarki dla wszystkich użytkowników (jeśli jednak masz kontrolę nad klientami użytkowników, na przykład w środowisku korporacyjnym, to super!). Pewnego dnia… ale na razie piaskownica to kolejny poziom zabezpieczeń, który wzmacnia Twoją obronę, ale nie jest to pełna obrona, na której możesz polegać. Warstwy są jednak świetne. Proponuję skorzystać z tego.

Więcej informacji

  • Privilege Separation in HTML5 Applications to interesujący artykuł, który opisuje zaprojektowanie niewielkiego frameworku i jego zastosowanie w 3 dotychczasowych aplikacjach HTML5.

  • Piaskownica może być jeszcze bardziej elastyczna, gdy połączysz ją z 2 nowymi atrybutami iframe: srcdocseamless. Pierwsza pozwala wypełnić ramkę treściami bez nakładania się żądania HTTP, a druga umożliwia przeniesienie stylu do treści w ramce. Oba te formaty mają obecnie dość słabe wsparcie w przeglądarkach (Chrome i WebKit Nightly), ale w przyszłości mogą być ciekawą kombinacją. Możesz na przykład dodać komentarz do artykułu w piaskownicy za pomocą tego kodu:

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