Stworzenie bogatego środowiska w dzisiejszej sieci prawie zawsze wymaga umieszczenia 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 widget – każda reklama, każdy widget 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. 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 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 wyświetlać się w dowolnym miejscu na stronie. Ich zakres jest ograniczony do obrysu 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 iframe
pozwala nam zawęzić ograniczenia dotyczące treści w ramkach. Możemy zlecić przeglądarce 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 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 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 przyznaliśmy 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
lubtarget="_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 (autofokusowanie elementów formularza, automatyczne odtwarzanie filmów itp.), są blokowane.
- Nie można uzyskać blokady wskaźnika.
- Atrybut
seamless
jest ignorowany w przypadkuiframes
zawartych w dokumencie z ramką.
To bardzo surowe podejście, a dokumenty wczytane do całkowicie odizolowanego środowiska iframe
stwarzają 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ć zaskakują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 zhttps://example.com/
zachowają dostęp do danych tego pochodzenia.allow-scripts
umożliwia wykonywanie kodu JavaScriptu, a także automatyczne uruchamianie funkcji (są one łatwe do zaimplementowania za pomocą JavaScriptu).allow-top-navigation
pozwala na wyjście dokumentu poza ramkę poprzez 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 musi 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 żadnych 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 sandboksie 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 z siecią, oraz wiele procesów mechanizmu renderowania o mniejszych uprawnieniach, które wykonują ciężką pracę związaną z analizowaniem niesprawdzonych treści. Renderery nie muszą dotykać dysku, ponieważ przeglądarka dostarcza im wszystkich informacji potrzebnych do renderowania 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 środka kodu, 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>
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. 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" && 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? Utworzyliśmy bardzo prosty interfejs API do oceny. Dzięki temu możemy mieć pewność, że oceniany kod nie ma dostępu do poufnych informacji, 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ż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 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-origin i allow-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 sforsowania 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
Artykuł „Privilege Separation in HTML5 Applications” (Oddzielenie uprawnień w aplikacjach HTML5) 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:
srcdoc
iseamless
. Pierwsza pozwala wypełnić ramkę treściami bez nakładów związanych z żądaniem 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 w wersji nightly), ale w przyszłości mogą stanowić ciekawe połączenie. 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>