Graj bezpiecznie w elementach iframe w trybie piaskownicy

Tworzenie bogatych treści we współczesnym internecie niemal nieuniknione wymaga umieszczania komponentów i treści, nad którymi nie masz żadnej 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. Rezygnowanie z jednego z tych rozwiązań nie jest dobrym rozwiązaniem, ale zwiększa ryzyko, że w witrynie może wydarzyć się coś BadTM. Każdy wbudowany widget, każda reklama, każdy widget mediów społecznościowych to potencjalny wektor ataku dla osób o złośliwych zamiarach:

Content Security Policy (CSP) może zmniejszyć ryzyko związane z obydwoma tymi typami treści, umożliwiając 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 jestem pewny, że naprawdę ufam temu źródłu treści, ale to naprawdę cudowne! Embed it please, Browser, but don't let it break my site."

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 szkodzi. Jeśli nie wymaga Flasha, wyłączenie obsługi wtyczek nie powinno stanowić problemu. Zapewniamy jednak, że 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ć. 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. Wczytywanie niezaufanych komponentów w interfejsie iframe pozwala zmierzyć odległość między aplikacją a treścią, którą 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 wyświetlać się w dowolnym miejscu na stronie. Ich zakres jest ograniczony do obrysu ramki. Podział nie jest jednak w pełni niezawodny. Zawarta w niej strona nadal może zachowywać się irytująco lub złośliwie, a autoodtwarzanie filmów, wtyczki i wyskakujące okienka to zaledwie wierzchołek góry lodowej.

Atrybut sandbox elementu iframe daje nam dokładnie to, czego potrzebujemy, aby zaostrzyć ograniczenia dotyczące treści w ramkach. Możemy nakazać przeglądarce wczytywanie zawartości określonej klatki w środowisku o niskich uprawnieniach, udostępniając tylko podzbiór funkcji niezbędnych do wykonywania określonych zadań.

Zaufaj, ale zweryfikuj

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 określić, co możemy zablokować, przyjrzyjmy się dokładnie, jakich funkcji 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. Musi on mieć dostęp do plików cookie Twittera, aby powiązać tweeta z właściwym kontem. Potrzebna jest też możliwość przesłania formularza. 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 zdolnościami

W przykładzie powyżej zauważyliśmy kilka możliwych flag piaskownicy. Przyjrzyjmy się teraz działaniu atrybutu bardziej szczegółowo.

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 testy tej samej domeny zakończą się niepowodzeniem. Unikalne źródła nie pasują do żadnego innego źródła ani nawet do niego samego. 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 przesł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 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, który zawiera dokument w ramce.

To bardzo surowe podejście, a dokumenty wczytane do całkowicie odizolowanego środowiska iframestanowią bardzo niewielkie zagrożenie. 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, każde z tych ograniczeń 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 przesyłanie formularzy.
  • allow-popups zezwala (co może być szokujące) na wyskakujące okienka.
  • allow-pointer-lock umożliwia (niespodzianka!) 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 JavaScript i umożliwia automatyczne wyzwalanie funkcji (ponieważ wdrożenie ich za pomocą JavaScriptu byłoby bardzo trudne).
  • allow-top-navigation pozwala na wyjście dokumentu poza ramkę, gdy użytkownik porusza się po 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.
  • Element allow-same-origin jest niezbędny, ponieważ pliki cookie twitter.com byłyby niedostępne i użytkownik nie mógł zalogować się, aby opublikować 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 żadnych negatywnych skutków. Jest 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 własnym kodem? Zaufanie sobie, 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 takim czy innym stopniu podatna na wykorzystanie. Piaskownica we własny kod oznacza, że nawet jeśli atakujący uda mu się obejść Twoją aplikację, nie będzie on miał pełnego dostępu do jej źródła. Będzie mógł wykonywać tylko te czynności, które może wykonywać aplikacja. Nadal nie tak, ale nie tak źle.

Możesz jeszcze bardziej zmniejszyć ryzyko, dzieląc aplikację na logiczne części i umieszczając każdą z nich w sandboksie z minimalnymi uprawnieniami. Ta technika jest bardzo popularna w kodzie natywnym. Na przykład Chrome przechodzi do procesu 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 renderowania o niskich uprawnieniach, które wykonują ciężkie zadanie przy analizie niezaufanych 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 aplikacji mogą znajdować się w piaskownicy iframe, a dokument nadrzędny może pośredniczyć w komunikacji między nimi, publikując wiadomości i odbierając odpowiedzi. Taka struktura powoduje, że luki w zabezpieczeniach w każdym elemencie aplikacji wyrządzają minimalne możliwe szkody. 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ę przykładowi zabawki, aby sprawdzić, jak to działa.

Evalbox to ciekawa aplikacja, która pobiera ciąg znaków i interpretuje go jako kod JavaScript. Wow, prawda? Tyle było tych, na co tak długo czekaliśmy. Jest to oczywiście dość niebezpieczna aplikacja, ponieważ umożliwienie wykonania dowolnego JavaScriptu oznacza, że wszelkie dane, jakie oferuje źródło, są gotowe do przechwycenia. 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 uruchomione, 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. Później wykonamy najcięższą pracę, przekazując przekazane nam dane do usługi eval(). To wywołanie jest objęte blokiem próbnym, ponieważ zablokowane operacje w iframe w piaskownicy często generują wyjątki DOM. Będziemy je wykrywać i wyświetlać odpowiedni komunikat o błędzie. Na koniec zwracamy wynik z powrotem do okna nadrzędnego. To całkiem 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>

Przekażemy kod do realizacji. 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 na 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? Stworzyliśmy bardzo prosty interfejs API oceny – możemy mieć pewność, że oceniany kod nie ma dostępu do informacji poufnych, takich jak pliki cookie czy 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ć z własnym kodem, dzieląc aplikacje monolityczne na komponenty jednozadaniowe. Każdy z nich może być opakowany 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 dyspozytora, wysyłając wiadomości do konkretnych modułów, z których każdy ma jak najmniejsze uprawnienia do wykonywania swoich zadań, nasłuchuje wyników i dba o to, aby każdy moduł miał dostęp tylko do potrzebnych informacji.

Pamiętaj jednak, że musisz bardzo ostrożnie obchodzić się z treściami w ramce, które pochodzą z tego samego źródła co rodzic. 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. Gdy podzielisz aplikację monolityczną na zestaw usług w trybie piaskownicy, z których każda odpowiada za mały fragment samodzielnych funkcji, osoby przeprowadzające atak będą musiały przejąć nie tylko zawartość określonych ramek, ale także ich kontroler. To znacznie trudniejsze zadanie, zwłaszcza że zakres działania kontrolera może być znacznie ograniczony. Jeśli chcesz poświęcić czas na sprawdzenie kodu pod kątem bezpieczeństwa, możesz skoncentrować się na tym, 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 przez 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.

Dalsza lektura

  • Rozdział uprawnień w aplikacjach HTML5” to interesujący artykuł na temat projektowania niewielkiej platformy i jego zastosowania w trzech istniejących aplikacjach HTML5.

  • Piaskownica może być jeszcze bardziej elastyczna, gdy połączysz ją z 2 nowymi atrybutami iframe: srcdocseamless. Pierwszy pozwala wypełnić ramkę treścią bez narzutu żądania HTTP, a drugi – przepuszczać styl do tej treści. Obie przeglądarki obsługują obecnie dość słabą jakość (Chrome i WebKit), ale w przyszłości będą interesującym połączeniem. Możesz np. umieścić komentarze 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>