Tworzenie atrakcyjnych treści w dzisiejszym internecie niemal nieuchronnie wiąże się z osadzaniem komponentów i treści, nad którymi nie masz rzeczywistej kontroli. Widżety innych firm mogą zwiększać zaangażowanie i odgrywać kluczową rolę w ogólnych wrażeniach użytkowników, a treści generowane przez użytkowników są czasami nawet ważniejsze niż treści natywne witryny. Rezygnacja z któregoś z tych rozwiązań nie wchodzi w grę, ale oba zwiększają ryzyko, że w Twojej witrynie może wydarzyć się Coś Złego™. Każdy umieszczony 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:
Standard Content Security Policy (CSP) może zmniejszyć ryzyko związane z obydwoma tymi rodzajami treści, umożliwiając dodanie do listy dozwolonych konkretnych zaufanych źródeł skryptów i innych treści. To duży krok we właściwym kierunku, ale warto zauważyć, że ochrona oferowana przez większość dyrektyw CSP jest binarna: zasób jest dozwolony lub nie. Czasami warto powiedzieć: „Nie mam pewności, czy ufam temu źródłu treści, ale jest ono takie ładne! „Przeglądarko, umieść to, ale nie pozwól, aby zepsuło moją witrynę”.
Jak najmniejsze uprawnienia
W zasadzie szukamy mechanizmu, który pozwoli nam przyznać osadzonym treściom tylko minimalny poziom uprawnień niezbędny do wykonywania ich zadań. Jeśli widżet nie musi otwierać nowego okna, odebranie dostępu do metody window.open nie zaszkodzi. Jeśli nie wymaga Flasha, wyłączenie obsługi wtyczek nie powinno być problemem. Jeśli będziemy przestrzegać zasady jak najmniejszych uprawnień i blokować każdą funkcję, która nie jest bezpośrednio związana z funkcjonalnością, z której chcemy korzystać, zapewnimy sobie maksymalne bezpieczeństwo. Dzięki temu nie musimy już ślepo ufać, że jakiś element umieszczonej treści nie wykorzysta uprawnień, których nie powinien używać. Po prostu nie będzie mieć dostępu do tej funkcji.
Elementy iframe to pierwszy krok w kierunku stworzenia dobrej struktury takiego rozwiązania.
Wczytanie niezaufanego komponentu w elemencie iframe zapewnia pewien stopień separacji między aplikacją a treścią, którą chcesz wczytać. Treść w ramce nie będzie miała dostępu do modelu DOM strony ani do danych przechowywanych lokalnie. Nie będzie też mogła wyświetlać się w dowolnych miejscach na stronie, ponieważ jej zakres jest ograniczony do obrysu ramki. Separacja nie jest jednak całkowita. Strona w ramce nadal może zawierać wiele irytujących lub szkodliwych elementów, takich jak automatycznie odtwarzane filmy, wtyczki i wyskakujące okienka.
Atrybut sandbox elementu iframe daje nam dokładnie to, czego potrzebujemy, aby zaostrzyć ograniczenia dotyczące treści w ramkach. Możemy poinstruować przeglądarkę, aby wczytywała zawartość określonej ramki w środowisku o niskich uprawnieniach, zezwalając tylko na podzbiór funkcji niezbędnych do wykonania danej pracy.
Stosuj zasadę ograniczonego zaufania
Przycisk „Tweet” na Twitterze to świetny przykład funkcji, którą można bezpieczniej umieścić w witrynie za pomocą piaskownicy. Twitter umożliwia osadzenie przycisku za pomocą elementu iframe z tym kodem:
<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
Aby dowiedzieć się, co możemy zablokować, przyjrzyjmy się dokładnie, jakich funkcji wymaga przycisk. Kod HTML wczytywany do ramki wykonuje fragment kodu JavaScript z serwerów Twittera i po kliknięciu generuje wyskakujące okienko z interfejsem do tweetowania. Ten interfejs potrzebuje dostępu do plików cookie Twittera, aby powiązać tweeta z odpowiednim kontem, oraz możliwości przesłania formularza tweetowania. To właściwie wszystko. Ramka nie musi wczytywać żadnych wtyczek, nie musi przechodzić do okna najwyższego poziomu ani wykonywać wielu innych czynności. Ponieważ nie potrzebuje tych uprawnień, usuńmy je, umieszczając zawartość ramki w piaskownicy.
Bezpieczna piaskownica działa na podstawie listy dozwolonych. Zaczynamy od usunięcia wszystkich możliwych uprawnień, a następnie ponownie włączamy poszczególne funkcje, dodając do konfiguracji piaskownicy określone flagi. W przypadku widżetu Twittera zdecydowaliśmy się włączyć JavaScript, wyskakujące okienka, przesyłanie formularzy i pliki cookie twitter.com. Możemy to zrobić, dodając do elementu iframe atrybut sandbox 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. Ramka ma wszystkie wymagane uprawnienia, a przeglądarka odmówi jej dostępu do uprawnień, których nie przyznano jej wyraźnie za pomocą wartości atrybutu sandbox.
Szczegółowa kontrola nad funkcjami
W przykładzie powyżej widzieliśmy kilka możliwych flag piaskownicy. Przyjrzyjmy się teraz bliżej wewnętrznemu działaniu atrybutu.
Jeśli element iframe ma pusty atrybut piaskownicy, dokument w ramce będzie w pełni odizolowany i będą obowiązywać te ograniczenia:
- JavaScript nie będzie wykonywany w dokumencie w ramce. Obejmuje to nie tylko kod JavaScript jawnie wczytywany za pomocą tagów skryptów, ale też wbudowane moduły obsługi zdarzeń i adresy URL javascript:. Oznacza to również, że zawartość tagów noscript będzie wyświetlana dokładnie tak, jakby użytkownik sam wyłączył skrypt.
- Dokument w ramce jest wczytywany do unikalnego źródła, co oznacza, że wszystkie testy 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 żadnego źródła ani w żadnych innych mechanizmach pamięci (pamięć DOM, Indexed DB itp.).
- Dokument w ramce nie może tworzyć nowych okien ani okien dialogowych (np. za pomocą
window.openlubtarget="_blank"). - Nie można przesyłać formularzy.
- Wtyczki nie będą się wczytywać.
- Dokument w ramce może nawigować tylko po sobie, a nie po dokumencie nadrzędnym najwyższego poziomu.
Ustawienie
window.top.locationspowoduje wyjątek, a kliknięcie linku ztarget="_top"nie przyniesie żadnego efektu. - Funkcje, które uruchamiają się automatycznie (elementy formularza z automatycznym ustawianiem ostrości, automatycznie odtwarzane filmy itp.), są blokowane.
- Nie można uzyskać blokady wskaźnika.
- Atrybut
seamlessjest ignorowany w przypadkuiframeszawartych w dokumencie w ramce.
To bardzo rygorystyczne rozwiązanie, a dokument wczytany do w pełni odizolowanego środowiska iframe stanowi bardzo niewielkie zagrożenie. Oczywiście nie może też zrobić nic wartościowego: w przypadku niektórych treści statycznych możesz użyć pełnej piaskownicy, ale w większości przypadków warto nieco poluzować ograniczenia.
Z wyjątkiem wtyczek każde z tych ograniczeń można usunąć, dodając flagę do wartości atrybutu sandbox. W dokumentach w piaskownicy nie można uruchamiać wtyczek, ponieważ są one kodem natywnym poza piaskownicą, ale wszystko inne jest dozwolone:
allow-formsumożliwia przesyłanie formularzy.allow-popupszezwala na wyskakujące okienka.allow-pointer-lockumożliwia (niespodzianka!) blokowanie wskaźnika.allow-same-originpozwala dokumentowi zachować źródło. Strony wczytane zhttps://example.com/zachowają dostęp do danych tego źródła.allow-scriptsumożliwia wykonywanie kodu JavaScript, a także automatyczne uruchamianie funkcji (ponieważ ich implementacja za pomocą JavaScriptu byłaby bardzo prosta).allow-top-navigationumożliwia dokumentowi wyjście poza ramkę przez przejście do okna najwyższego poziomu.
Mając to na uwadze, możemy dokładnie ocenić, dlaczego w powyższym przykładzie z Twitterem otrzymaliśmy konkretny zestaw flag piaskownicy:
- Atrybut
allow-scriptsjest wymagany, ponieważ strona wczytywana do ramki uruchamia kod JavaScript, który obsługuje interakcje użytkownika. - Atrybut
allow-popupsjest wymagany, ponieważ przycisk otwiera formularz tweetowania w nowym oknie. - Atrybut
allow-formsjest wymagany, ponieważ formularz do tweetowania powinien być możliwy do przesłania. allow-same-originjest konieczne, ponieważ w przeciwnym razie pliki cookie twitter.com byłyby niedostępne i użytkownik nie mógłby się zalogować, aby przesłać formularz.
Ważne jest, że flagi piaskownicy zastosowane do ramki są również 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, które otwiera ramka.
Po dodaniu atrybutu sandbox widżet otrzymuje tylko wymagane uprawnienia, a funkcje takie jak wtyczki, górna nawigacja i blokada wskaźnika pozostają zablokowane. Zmniejszyliśmy ryzyko związane z umieszczaniem widżetu, nie powodując żadnych negatywnych skutków.
To świetny układ dla każdej ze stron.
Rozdzielenie uprawnień
Umieszczanie treści osób trzecich w piaskownicy w celu uruchamiania ich niezaufanego kodu w środowisku o niskich uprawnieniach jest dość oczywistą korzyścią. A co z Twoim własnym kodem? Ufasz sobie, prawda? Dlaczego więc warto korzystać z trybu piaskownicy?
Odwróciłbym to pytanie: jeśli Twój kod nie potrzebuje wtyczek, po co dawać mu do nich dostęp? W najlepszym przypadku jest to uprawnienie, którego nigdy nie używasz, a w najgorszym – potencjalny wektor, który może ułatwić atakującym dostęp do Twoich danych. Każdy kod zawiera błędy i praktycznie każda aplikacja jest podatna na wykorzystanie w ten czy inny sposób. Izolowanie własnego kodu oznacza, że nawet jeśli atakujący zdoła przejąć kontrolę nad aplikacją, nie uzyska pełnego dostępu do jej źródła. Będzie mógł robić tylko to, co może robić aplikacja. To nadal źle, ale nie tak źle, 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 piaskownicy 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 renderowania o niskich uprawnieniach, które wykonują ciężką pracę polegającą na analizowaniu niezaufanych treści. Procesy renderowania nie muszą korzystać z dysku, ponieważ przeglądarka przekazuje im wszystkie informacje potrzebne do wyrenderowania strony. Nawet jeśli sprytny haker znajdzie sposób na uszkodzenie mechanizmu renderowania, nie osiągnie zbyt wiele, ponieważ sam mechanizm renderowania nie może zrobić niczego interesującego: wszystkie operacje wymagające wysokich uprawnień 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 ataku.
Bezpieczne umieszczanie w piaskownicy: eval()
Dzięki piaskownicy i interfejsowi API postMessage ten model można łatwo zastosować w internecie. Części aplikacji mogą znajdować się w elementach iframe w piaskownicy, a dokument nadrzędny może pośredniczyć w komunikacji między nimi, wysyłając wiadomości i oczekując na odpowiedzi. Taka struktura gwarantuje, że luki w zabezpieczeniach w dowolnej części aplikacji wyrządzą minimalne szkody. Ma to też tę zaletę, że wymusza tworzenie przejrzystych punktów integracji, dzięki czemu dokładnie wiesz, gdzie należy zachować ostrożność podczas sprawdzania danych wejściowych i wyjściowych. Przyjrzyjmy się prostemu przykładowi,
aby zobaczyć, jak to działa.
Evalbox to ciekawa aplikacja, która pobiera ciąg znaków i ocenia go jako JavaScript. Niesamowite, prawda? To, na co czekasz od tylu lat. Jest to oczywiście dość niebezpieczna aplikacja, ponieważ zezwolenie na wykonywanie dowolnego kodu JavaScript oznacza, że wszystkie dane, które ma do zaoferowania źródło, są dostępne dla każdego. Zmniejszymy ryzyko wystąpienia niepożądanych zdarzeń, zapewniając, że kod będzie wykonywany w piaskownicy, co znacznie zwiększy bezpieczeństwo. Przeanalizujemy kod od środka, 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ę minimalny dokument, który po prostu nasłuchuje wiadomości z elementu nadrzędnego, podłączając się do zdarzenia message obiektu window.
Za każdym razem, gdy element nadrzędny wykonuje postMessage na zawartości elementu iframe, wywoływane jest to zdarzenie, które daje nam dostęp do ciągu znaków, który element nadrzędny chce, abyśmy wykonali.
W programie obsługi pobieramy atrybut source zdarzenia, który jest oknem nadrzędnym. Użyjemy go do przesłania wyników naszej ciężkiej pracy, gdy skończymy. Następnie wykonamy najtrudniejszą część, przekazując otrzymane dane do eval(). To wywołanie zostało umieszczone w bloku try, ponieważ niedozwolone operacje w piaskownicy iframe często generują wyjątki DOM. Przechwycimy je i wyświetlimy przyjazny komunikat o błędzie. Na koniec publikujemy wynik
z powrotem w oknie nadrzędnym. To całkiem proste.
Element nadrzędny jest równie prosty. Utworzymy niewielki interfejs z elementem textarea na kod i elementem button do wykonywania kodu. Pobierzemy też frame.html za pomocą elementu iframe w piaskownicy, który umożliwia tylko wykonywanie skryptów:
<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
id='sandboxed'
src='frame.html'></iframe>
Teraz połączymy wszystko, aby umożliwić wykonanie. Najpierw będziemy nasłuchiwać odpowiedzi z iframe i alert() je naszym użytkownikom. Prawdopodobnie prawdziwa aplikacja
robiłaby coś mniej irytującego:
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 podłączymy moduł obsługi zdarzeń do kliknięć elementu button. Gdy użytkownik kliknie, pobierzemy bieżącą zawartość elementu textarea i przekażemy ją do ramki w celu 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? Utworzyliśmy bardzo prosty interfejs API do oceny i możemy mieć pewność, że oceniany kod nie ma dostępu do informacji poufnych, takich jak pliki cookie czy pamięć DOM. Podobnie oceniany kod nie może wczytywać wtyczek, otwierać nowych okien ani wykonywać innych uciążliwych lub szkodliwych działań.
Możesz zrobić to samo z własnym kodem, dzieląc aplikacje monolityczne na komponenty o jednym przeznaczeniu. Każdy z nich można opakować w prosty interfejs API do obsługi wiadomości, taki jak ten, który opisaliśmy powyżej. Okno nadrzędne o wysokich uprawnieniach może działać jako kontroler i dyspozytor, wysyłając wiadomości do określonych modułów, z których każdy ma najmniejsze możliwe uprawnienia do wykonywania swoich zadań, nasłuchując wyników i zapewniając, że każdy moduł otrzymuje tylko te informacje, których potrzebuje.
Pamiętaj jednak, że musisz zachować szczególną ostrożność, gdy masz do czynienia z treściami w ramkach, które pochodzą z tego samego źródła co element nadrzędny. Jeśli strona w domenie https://example.com/ umieszcza w ramce inną stronę z tego samego źródła z piaskownicą, która zawiera flagi allow-same-origin i allow-scripts, strona w ramce może dotrzeć do strony nadrzędnej i całkowicie usunąć atrybut piaskownicy.
Graj w piaskownicy
Tryb piaskownicy jest obecnie dostępny w różnych przeglądarkach: Firefox 17+, IE10+ i Chrome (aktualną tabelę obsługi znajdziesz na stronie caniuse). Zastosowanie atrybutu sandbox do elementu iframes pozwala przyznać pewne uprawnienia do wyświetlanych treści, ale tylko te, które są niezbędne do prawidłowego działania treści. Dzięki temu możesz zmniejszyć ryzyko związane z uwzględnianiem treści osób trzecich, wykraczając poza to, co jest już możliwe dzięki zasadom bezpieczeństwa treści.
Ponadto piaskownica to skuteczna technika zmniejszania ryzyka, że sprytny haker będzie mógł wykorzystać luki w Twoim kodzie. Dzięki rozdzieleniu aplikacji monolitycznej na zestaw usług w piaskownicy, z których każda odpowiada za niewielką część samodzielnej funkcji, atakujący będą musieli nie tylko przejąć zawartość określonych ramek, ale także ich kontroler. To znacznie trudniejsze zadanie, zwłaszcza że kontroler może mieć znacznie mniejszy zakres. Możesz poświęcić czas na sprawdzenie tego kodu, jeśli poprosisz przeglądarkę o pomoc z resztą.
Nie oznacza to, że piaskownica jest kompletnym rozwiązaniem problemu bezpieczeństwa w internecie. Zapewnia dogłębną obronę i dopóki 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 kontrolujesz klientów użytkowników – na przykład w środowisku firmowym – to świetnie!). Kiedyś… ale na razie piaskownica to kolejna warstwa zabezpieczeń, która wzmacnia obronę, ale nie jest kompletnym rozwiązaniem, na którym można polegać. Mimo to warstwy są świetne. Sugeruję skorzystanie z tego.
Więcej informacji
„Privilege Separation in HTML5 Applications” (Separacja uprawnień w aplikacjach HTML5) to interesujący artykuł, który opisuje projekt niewielkiego frameworka i jego zastosowanie w 3 istniejących aplikacjach HTML5.
Piaskownica może być jeszcze bardziej elastyczna, gdy połączy się ją z 2 innymi nowymi atrybutami elementu iframe:
srcdociseamless. Pierwszy z nich umożliwia wypełnienie ramki treścią bez obciążenia związanego z żądaniem HTTP, a drugi pozwala na zastosowanie stylu do treści w ramce. Obecnie obie te funkcje są obsługiwane przez niewiele przeglądarek (Chrome i WebKit Nightly), ale w przyszłości będą stanowić interesujące połączenie. Możesz na przykład 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>