Podstawy#

Pochodzenie zasobów#

Strona internetowa to zbiór wielu powiązanych ze sobą zasobów, do których zaliczyć można źródłowy dokument jak i dołączane do niego arkusze stylów, skrypty, multimedia czy inne dokumenty (w ramkach). Zasoby wskazywane są za pomocą adresów URI, których budowa determinuje tzw. pochodzenie. Jest to bardzo ważna kwestia i prędzej czy później każdy programista webowy musi się z nią zapoznać.

Opis powstał na bazie następujących ogólnodostępnych materiałów:

Całość stanowi jedynie podstawę przygotowującą grunt dla bardziej konkretyzujących zagadnień, które w niedalekiej przyszłości mam zamiar opisać.

Wprowadzenie#

Aplikacje klienckie wchodzą w interakcje z zawartością/treścią (content) utworzoną przez dużą liczbę autorów. Mimo że wielu z tych autorów ma dobre intencje, to niektórzy z nich mogą generować zagrożenia. Z racji tego, że aplikacje klienckie przeprowadzają działania w oparciu o zawartość którą przetwarzają, autorzy aplikacji klienckich mogą zechcieć ograniczyć negatywne oddziaływanie szkodliwych autorów, zdolnych do zaburzenia poufności lub integralności treści czy serwerów.

Rozważmy dla przykładu aplikację kliencką HTTP, która renderuje zawartość HTML pobieraną z różnych serwerów. Jeśli aplikacja kliencka wykonuje skrypty zawarte w tych dokumentach, to autorzy aplikacji klienckiej mogą zablokować dla skryptów pobieranych ze złośliwego serwera możliwość czytania dokumentu z zaufanego serwera, który może, dla przykładu, znajdować się za zaporą.

W ujęciu ogólnym aplikacje klienckie dzielą zawartość zgodnie z jej pochodzeniem. Konkretyzując, aplikacje klienckie umożliwiają zawartości pobieranej z jednego pochodzenia swobodnie wchodzić w interakcje z innymi zawartościami pobranymi z tego samego pochodzenia, ale aplikacje klienckie ograniczają sposób, w jaki ta zawartość może oddziaływać z zawartością o innym pochodzeniu.

Definicja pochodzenia#

Ogólnie rzecz biorąc aplikacje klienckie mogą traktować każdy adres URI w ramach oddzielnej ochrony domenowej i wymagać wyraźnej zgody dla zawartości pobranej z jednego adresu URI, aby możliwa była interakcja z innym adresem URI. Niestety taki model jest kłopotliwy dla deweloperów, gdyż aplikacje webowe bardzo często składają się z wielu zasobów, które wzajemnie na siebie oddziałują.

Zamiast tego aplikacje klienckie grupują adresy URI razem, w ramach ochrony domenowej zwanej pochodzeniem # (origin). Z grubsza rzecz biorąc dwa adresy URI są częścią tego samego pochodzenia (tzn. reprezentują te same zasady) jeśli mają identyczny schemat, host i port (jeśli podano).

Warto przypomnieć, że port 80 jest domyślny na wielu popularnych serwerach, to też nie musi być jawnie podawany w adresach URI.

Jeśli pochodzenia nie można określić na podstawie adresu URI to musi nim zostać unikatowy identyfikator globalny # (globally unique identifier), czyli wartość inna od wszystkich istniejących do tej pory wartości. Może nią zostać wystarczająco długi losowy łańcuch znakowy.

Ogólne zasady ustalania i porównywania pochodzeń opisywane są w dokumencie "RFC 6454", chociaż szczegóły mogą być konkretyzowane w innych specyfikacjach, takich jak HTML5. Tak czy inaczej rzeczywista implementacja będzie zależna od danego programu.

Poniższa tabela zawiera praktyczne porównanie pochodzeń kilku różnych adresów URL względem początkowego "http://www.example.com/dir/page.html":

Porównywany URLZgodnośćObjaśnienie
http://www.example.com/dir/page2.htmlTakIdentyczny schemat i host.
http://www.example.com/dir2/other.htmlTakIdentyczny schemat i host.
http://username:password@www.example.com/dir2/other.htmlTakIdentyczny schemat i host.
httpː//www.example.com:81/dir/other.htmlNieInny port.
https://www.example.com/dir/other.htmlNieInny schemat.
http://en.example.com/dir/other.htmlNieInny host.
http://example.com/dir/other.htmlNieInny host (wymagane dokładne dopasowanie).
http://v2.www.example.com/dir/other.htmlNieInny host (wymagane dokładne dopasowanie).
http://www.example.com:80/dir/other.htmlNie używajJawny port. Obsługa zależy od danej implementacji w przeglądarce.

Dlaczego nie używać jedynie hosta?#

Włączenie schematu do pochodzenia ma zasadnicze znaczenie dla bezpieczeństwa. Jeśli aplikacje klienckie nie uwzględniłyby schematu, to nie byłoby izolacji między adresami "http://example.com" i "https://example.com", ponieważ obydwa maja identyczne hosty. Jednakże, bez takiej izolacji, atakujący mógłby zmodyfikować zawartość pobieraną z "http://example.com" w taki sposób, że zawartość ta mogłaby poinstruować aplikację kliencką aby złamała poufność i integralność zawartości pobieranej z "https://example.com", z pominięciem ochrony wynikającej z TLS.

Dlaczego używać pełnej kwalifikowanej nazwy hosta zamiast jedynie domeny "top-level"?#

Chociaż system DNS posiada hierarchiczną delegację, to i tak zaufane relacje pomiędzy nazwami hostów zależą od wdrożenia. Dla przykładu, w wielu instytucjach edukacyjnych, studenci mogą udostępniać treść pod adresem "https://example.edu/~student/", ale nie oznacza to wcale, że dokument tworzony przez studenta powinien być częścią tego samego pochodzenia, co aplikacja webowa zarządzająca grupą studentów, udostępniana pod adresem "https://grades.example.edu/".

Wyjątki Internet Explorera#

Większość przeglądarek internetowych ustala pochodzenie zgodnie z obowiązującymi standardami. Oczywiście inaczej jest w przypadku IE, gdzie występują dwie zasadnicze różnice:

Polityka tego samego pochodzenia#

Wiele aplikacji klienckich podejmuje akcje na rzecz zdalnych części. Dla przykładu, aplikacje klienckie HTTP wykonują przekierowania, które są instrukcjami ze zdalnych serwerów, i aplikacje klienckie HTML prezentują interfejsy bogatego modelu DOM dla skryptów pobranych ze zdalnych serwerów.

Bez jakiegokolwiek modelu bezpieczeństwa aplikacje klienckie mogą podejmować akcje szkodliwe dla użytkownika lub innych części. Z biegiem czasu wiele powiązanych technologii webowych wypracowało wspólny model bezpieczeństwa potocznie określany jako polityka tego samego pochodzenia # (same-origin policy), w skrócie SOP.

SOP bazuje na pochodzeniu, które decyduje o zaufaniu w oparciu o URI. Rozważmy następujący skrypt HTML:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script src="https://example.com/library.js"></script>

Kiedy aplikacja kliencka przetwarza ten element to najpierw pobiera skrypt zgodnie z podanym URI i wykonuje go zgodnie z przywilejami dokumentu. W ten sposób dokument przydziela wszystkie swoje uprawnienia dla zasobu wskazywanego przez URI. Tym samym dokument stwierdza, że ufa integralności informacji otrzymywanej z tego URI.

Oprócz importowania bibliotek o podanym URI aplikacje klienckie mogą również wysyłać informacje do zdalnych części o określonym URI. Rozważmy następujący formularz HTML:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<form method="POST" action="https://example.com/login">
	<input type="password">
</form>

Kiedy użytkownik wprowadzi swoje hasło i zatwierdzi formularz to aplikacja kliencka wysyła hasło do końcowego punktu sieci wskazywanego przez URI. W ten sposób dokument wysyła ukryte dane do tego URI, co w istocie oznacza, że ufa integralności informacji wysyłanej do tego URI.

Autorytet#

Chociaż aplikacje klienckie grupują adresy URI do określonych pochodzeń to nie wszystkie zasoby w pochodzeniu wnoszą autorytet. Dla przykładu, obraz (element <img>) jest pasywną zawartością, a zatem nie wnosi autorytetu, co oznacza, że obraz nie ma dostępu do obiektów i zasobów dostępnych dla swojego pochodzenia. Z drugiej strony dokument HTML wnosi pełny autorytet swojego pochodzenia, i skrypty w dokumencie (lub zaimportowane do niego) mają dostęp do każdego zasobu w jego pochodzeniu.

Aplikacje klienckie określają zakres przydzielania autorytetu dla źródła na podstawie jego typu medialnego (typu MIME). Dla przykładu, źródła z typem medialnym image/png są traktowane jak obrazy, a źródła z typem medialnym text/html są traktowane jak dokumenty HTML.

W przypadku hostowania niezaufanych treści (np. generowanych przez użytkowników), aplikacje webowe mogą ograniczać autorytet zawartości poprzez ustawienie typu medialnego na image/png. Serwowanie generowanych treści użytkowników jako image/png jest mniej ryzykowne, niż serwowanie tychże treści jako text/html. Oczywiście wiele aplikacji webowych wciela niezaufane treści do swoich dokumentów HTML. Jeśli nie przeprowadzi się tego starannie to aplikacje ryzykują wyciekiem swojego autorytetu pochodzenia do niezaufanych treści, co potocznie nazywane jest atakiem typu cross-site scripting.

W celu zachowania zgodności z serwerami, które dostarczają nieprawidłowych typów medialnych, niektóre aplikacje klienckie wprowadzają analizowanie zawartości (content sniffing) i traktują zawartość tak, jak gdyby miała inny typ medialny od tego, który został wysłany przez serwer. Jeśli nie zostanie to wykonane prawidłowo to może wygenerować luki w zabezpieczeniach ponieważ aplikacje klienckie mogą nadać typom medialnym o niskim autorytecie, takim jak obrazy, przywileje właściwe dla typów medialnych o wysokim autorytecie, takim jak dokumenty HTML.

Więcej szczegółów odnośnie analizowania zawartości znajduje się w roboczym dokumencie "MIME Sniffing Living Standard".

Polityka#

Ogólnie rzecz biorąc aplikacje klienckie izolują różne pochodzenia i zezwalają na kontrolowanie komunikacji między pochodzeniami. Szczegóły tego mechanizmu zależą od kilku czynników.

Dostęp do obiektów#

Większość obiektów (stanowiących interfejs programowania aplikacji, w skrócie API) aplikacje klienckie udostępniają tylko dla tego samego pochodzenia. Konkretyzując, zawartość pobrana z jednego adresu URI może mieć dostęp do obiektów powiązanych z zawartością o inny adresie URI jedynie wtedy, kiedy obydwa adresy URI należą do tego samego pochodzenia.

Istnieją pewne wyjątki od tej ogólnej zasady. Dla przykładu, niektóre części z HTML-owego interfejsu Location są dostępne między pochodzeniami (np. w celu umożliwienia nawigacji dla innych kontekstów przeglądania, takich jak ramki). Podobnie jest w przypadku HTML-owego interfejsu postMessage, który jest widoczny między pochodzeniami w celu umożliwienia skrośnej komunikacji między nimi. Odsłanianie obiektów dla obcych pochodzeń jest niebezpieczne i powinno być wykonywane z dużą dozą ostrożności, ponieważ konsekwencją tego jest jednoczesne odsłanianie obiektów dla potencjalnych napastników.

Dostęp do Sieci#

Dostęp do zasobów sieciowych zależy od tego, czy zasoby te są w identycznym pochodzeniu, co zawartość próbująca uzyskać do nich dostęp.

W ogólnym ujęciu czytanie informacji z innego pochodzenia jest zabronione. Istnieją przypadki, gdzie pochodzenie zezwala na używanie niektórych rodzajów zasobów pobieranych z innych pochodzeń. Dla przykładu, pochodzenie zezwala na wykonywanie skryptów, renderowanie obrazów i aplikowanie arkuszy stylów, które pobrano z innych pochodzeń. Pochodzenie może także wyświetlać zawartość z innego pochodzenia, jak w przypadku dokumentu HTML w ramce. Zasoby sieciowe mogą także decydować o możliwości odczytywania swoich informacji przez inne pochodzenia, choćby przy użyciu mechanizmu CORS.

Przesyłanie informacji do innego pochodzenia jest dozwolone. Trzeba jednak pamiętać, że wysyłanie informacji w obrębie sieci w dowolnych formatach jest niebezpieczne. Z tego powodu aplikacje klienckie ograniczają dokumenty do wysyłania informacji przy użyciu konkretnych protokołów, jak w żądaniach HTTP bez domyślnych nagłówków. Rozszerzanie zestawu dopuszczalnych protokołów, np. dodanie wsparcia dla WebSockets, musi być wykonane starannie, tak aby uniknąć wprowadzenia niebezpiecznych luk ("RFC 6455 - The WebSocket Protocol").

Praktyczne przełożenie#

Tyle jeśli chodzi o suchą teorię. Istnieje sporo niewłaściwych informacji na temat SOP, powielanych na wielu forach programistycznych. Oto kilka z nich:

W każdym z tych mitów jest pewne ziarno prawdy, ale nie powinno być niespodzianką, że jak zwykle diabeł tkwi w szczegółach.

W ramach uporządkowania i łatwiejszego przyswojenia informacji całości można przedstawić w kontekście dobrze znanego modelu zezwoleń RWX (Read, Write, Execute) z systemu plików, np. w NTFS. Zgodnie z modelem RWX użytkownikowi lub procesowi zezwala się na wykonanie wymienionych operacji na danym pliku. Analogicznie można potraktować SOP, gdzie pochodzenie "A" ma następujące zezwolenia:

Osoby obyte z technologiami informatycznymi nie powinny mieć problemów ze zrozumieniem analogii między modelem RWX a modelem SOP. Przeanalizujmy najważniejsze jego kwestie.

Blokada odczytu#

Skrypt uruchomiony z pochodzeniem "A" nie może mieć dostępu do treści z pochodzenia "B" w taki sposób, że zasób z "B" mógłby zostać skutecznie odbudowany przez "A". Zatem strona internetowa z pochodzeniem "A":

  1. Może uruchomić (wykonać) skrypty z "B".
  2. Nie może uzyskać dostępu do źródłowego kodu tego skryptu.
  3. Może zastosować (wykonać) arkusz stylów CSS z "B".
  4. Nie może uzyskać dostępu do źródłowego kodu tego arkusza stylów CSS.
  5. Może przetworzyć (wykonać) ramki wskazujące na stronę HTML z "B".
  6. Nie może uzyskać dostępu do źródłowego kodu tej ramki.
  7. Może wyświetlić (wykonać) obraz z "B".
  8. Nie może uzyskać dostępu do danych binarnych tego obrazu.
  9. Może uruchomić (wykonać) film z "B".
  10. Nie może zrekonstruować tego filmu poprzez przechwycenie poszczególnych jego klatek.
  11. itd.

Wykonanie najczęściej jest po prostu utożsamiane z osadzaniem (embedding) danego zasobu.

Każdy przypadek blokady odczytu jest dość oczywiste w teorii, ale z chwilą kiedy platforma webowa staje się coraz bardziej złożona, trudność w zakresie egzekwowania tych ograniczeń nieustannie rośnie.

Zacznijmy od prostego przykładu: ktoś mógłby zaproponować udostępnienie metody getPixel(x, y). Metoda pobierałaby punkt na ekranie i zwracała kolor piksela tego punktu. Oczywiście może być to przydatne w wielu sytuacjach, ale kiedy przyjrzymy się dokładniej, to złamane zostałyby reguły #8 i #10, oraz z dużym prawdopodobieństwem naruszone (w pośredni sposób) reguły #2, #4 i #6.

Istnienie metody getPixel(x, y) oznaczałoby, że atakujący mógłby osadzić ramkę, obraz czy film ze strony ofiary, a następnie użyć metody do odczytania wszystkich pikseli tego zasobu, pozwalając tym samym na rekonstrukcje jego zawartości. Po tym wszystkim strona atakującego mogłaby wysłać uzyskane w ten sposób dane do serwera atakującego.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<p>Pochodzenie dokumentu: "http://www.crimsteam.site90.net"</p>
<p>
	<span>Pierwsza ramka: "http://www.crimsteam.site90.net"</span>
	<span style="margin-left: 20px;">Druga ramka: "http://crimsteam.site90.net"</span>
</p>
<iframe src="http://www.crimsteam.site90.net" name="ramka1" style="display: inlinne; height: 100px;"></iframe>
<iframe src="http://crimsteam.site90.net" name="ramka2" style="display: inlinne; height: 100px; margin-left: 50px;"></iframe>
<p id="info"></p>

<script>

	var info = document.getElementById("info");
	var ramka1 = document.getElementsByName("ramka1")[0];
	var ramka2 = document.getElementsByName("ramka2")[0];
	var result1 = "";
	var result2 = "";

	// Identyczne pochodzenia ramki i dokumentu
	ramka1.onload = function(){

		result1 += "ramka1.name: " + ramka1.name
			+ "<br>" + "ramka1.style.height: " + ramka1.style.height
			+ "<br>" + "ramka1.src: " + ramka1.src
			+ "<br>" + "ramka1.contentDocument: " + ramka1.contentDocument; // [object HTMLDocument] - mamy dostęp

		info.innerHTML += result1;

	}

	// Różne pochodzenia ramki i dokumentu
	ramka2.onload = function(){

		result2 += "<br><br>" + "ramka2.name: " + ramka2.name
			+ "<br>" + "ramka2.style.height: " + ramka2.style.height
			+ "<br>" + "ramka2.src: " + ramka2.src
			+ "<br>" + "ramka2.contentDocument: " + ramka2.contentDocument; // null - brak dostępu

		info.innerHTML += result2;

	}

</script>

Interesujące jest to, że w obrębie tej samej właściwości możemy mieć do czynienia z różnymi przywilejami dla odczytu i zapisu. Tak dzieje się chociażby dla właściwości window.location lub window.location.href w ramkach, gdzie odczyt jest zabroniony, ale zmiana wartości jest dopuszczalna.

Ramki to specyficzny przypadek. Wczytywanie stron do ramek nigdy nie stanowiło problemu (niezależnie od pochodzenia), ale sytuacja zaczyna się zmieniać. Wszystko za sprawą dedykowanego nagłówka HTTP X-Frame-Options, który zezwala bądź nie zezwala na taki manewr. Aktualne przeglądarki internetowe już zaimplementowały nowe rozwiązanie i coraz częściej można się z nim spotkać na popularnych stronach internetowych (np. w wyszukiwarce Google). Po więcej szczegółów odsyłam bezpośrednio do dokumentacji "RFC 7034 - HTTP Header Field X-Frame-Options" lub zasobów Mozilli.

Ograniczenie zapisu#

Zapis oznacza wysłanie zawartości z jednego pochodzenia do innego. Może przyjmować następujące formy:

Niektóre z powyższych możliwości, zgodnie z SOP, mogą być wykonywane między różnymi pochodzeniami (np. nawigacja po URL-ach czy obsługa formularzy), podczas gdy inne na to nie zezwalają (np. żądania Ajaksa lub modyfikacja DOM w ramkach). Z biegiem czasu powstało kilka dodatkowych mechanizmów, dzięki którym można kontrolować poziom ograniczeń (np. CORS lub nowy atrybuty sandbox w ramkach).

Całkowita blokada zapisu (jak ma to miejsce przy odczycie), choć możliwa i bardzo bezpieczna, byłaby dużym ograniczeniem dla obecnych rozwiązań webowych. Izolacja każdej strony blokowałaby wymianę informacji między różnymi serwisami i w zasadzie sprowadzałaby się do komunikacji między jednym i tym samym pochodzeniem. Lepiej eliminować tylko te aspekty, które stwarzają największe zagrożenia, ale bez nadmiernego ograniczania użyteczności całego ekosystemu.

Obejścia#

SOP, przy całej swojej słuszności związanej z bezpieczeństwem, bardzo często bywa po prostu irytujący. Załóżmy, że mamy witrynę, na której chcielibyśmy wyświetlić informację o kursie dolara. Informacja ta pobierana byłaby z innej witryny (inna domena), do której nie mamy żadnego dostępu. Zgodnie z przytoczonymi do tej pory informacjami, nie jest to możliwe. Żądanie zasobu obcego pochodzenia przy użyciu Ajaksa nie powiedzie się, wczytanie witryny do ramki jest możliwe (choć nie zawsze), ale pobranie konkretnych informacji z ramki już nie.

Większość obejść dla SOP jest po prostu błędami, które prędzej czy później zostają wyeliminowane. W przypadku dostępu do obydwu witryn (serwerów) problem można rozwiązać dzięki najnowszym mechanizmom HTML5, ale sęk w tym, że dostępu zazwyczaj nie ma. Jeśli zewnętrzna witryna nie zezwala na skrośną komunikację, lub nie udostępnia specjalnego API do pobierania konkretnych informacji, to najczęstsze rozwiązanie polega na uruchomienie skryptu po stronie naszego serwera (np. PHP), który pobierze i zwróci interesujące nas informacje. Serwer nie jest obarczony przeglądarkowymi restrykcjami SOP, dlatego może wysyłać żądania do dowolnych stron.

Tworzenie dodatkowego kodu po stronie serwera zajmuje czas i opóźnia testowanie. Nowoczesne przeglądarki internetowe zezwalają na wyłączenie niektórych ograniczeń. W przypadku lokalnego testowania warto z nich skorzystać. Oto garść przydatnych informacji:

Firefox

Wyłączenie SOP: about:config >> security.fileuri.strict_origin_policy >> false

Wymuszenie CORS w nagłówkach odpowiedzi: rozszerzenie Force CORS (oryginał), Fork 1, Fork 2

Chrome

Wyłączenie SOP: Uruchom program z parametrem --disable-web-security

Wymuszenie CORS w nagłówkach odpowiedzi: rozszerzenie ForceCORS (GitHub)

Lokalny dostęp do ramek: Uruchom program z parametrem --allow-file-access-from-files

Internet Explorer

Wyłączenie lokalnego ostrzeżenia dla uruchamianych skryptów: Opcje internetowe >> Zaawansowane >> Zezwalaj zawartości aktywnej na działanie w plikach na moim serwerze

Pasek społecznościowy

SPIS TREŚCI AKTUALNEJ STRONY

Podstawy (H1) Pochodzenie zasobów (H2) Wprowadzenie (H3) Definicja pochodzenia (H3) Dlaczego nie używać jedynie hosta? (H4) Dlaczego używać pełnej kwalifikowanej nazwy hosta zamiast jedynie domeny "top-level"? (H4) Wyjątki Internet Explorera (H4) Polityka tego samego pochodzenia (H3) Autorytet (H4) Polityka (H4) Dostęp do obiektów (H5) Dostęp do Sieci (H5) Praktyczne przełożenie (H3) Blokada odczytu (H4) Ograniczenie zapisu (H4) Obejścia (H4)