Podczas pisania aplikacji ciągle musimy zwracać uwagę na bezpieczeństwo danych, które są przesyłane między klientem a serwerem. Oprócz mechanizmów, które sami implementujemy istnieją też te zaimplementowane natywnie w przeglądarce. To o czym dziś chciałbym powiedzieć to jeden z takich mechanizmów czyli SOP oraz sposób na jego rozluźnienie czyli CORS.
Czym jest SOP
Zacznijmy od mechanizmu zabezpieczającego, który jest zaimplementowany w przeglądarce czyli SOP - Same-Origin Policy. Mechanizm ten polega, iż tylko strony posiadające to samo pochodzenie (origin) mogą komunikować między sobą i wymieniać dane między sobą (najczęściej jest to komunikacja na drodze klient - serwer gdzie to klient chce dane istniejące na serwerze). Jednak kiedy możemy powiedzieć, iż strony (aplikacje) mają to samo pochodzenie? Do określenia tego służą nam 3 elementy :
- Protokół
- Host
- Port
Jeśli te 3 elementy będą identyczne to znaczy, iż strony mają ten sam Origin. Żeby lepiej to zobrazować mam parę przykładów:
- http://example.com i http://example.com/page/1 - OK
- http://example.com i https://example.com - inny protokół (HTTP i HTTPS)
- http://example.com i http://example.com:8080 - inny port (domyślnie http:// jest na porcie 80)
- http://example.com i http://posts.example.com - inny host
Jeśli to sprawdzenie nie przejdzie (czyli przynajmniej jeden z elementów będzie różny) to przeglądarka zablokuje taką komunikację i klient nie dostanie danych o jakie prosi.
Czym jest CORS
Skoro wiemy czym jest SOP to teraz mechanizm, który pozwala nam to osłabić czyli CORS - Cross-Origin Resource Sharing. Jednak skoro SOP ma nas chronić to po co mechanizm, który będzie go osłabiał? Jest to spowodowane sposobem w jaki tworzymy aktualnie aplikacje internetowe. Obstawiam, iż większość aktualnie tworzonych aplikacji jest tworzona jako SPA - czyli mamy kod Javascript, który komunikuje się przy pomocy odpowiednich zapytań z serwerem. No i w związku z tym, iż są to osobne aplikacje (frontend i backend) to nie będą spełniały wymogu posiadania identycznego Originu. Bez CORS'a takie aplikacje nie mogłyby funkcjonować.
CORS jest mechanizmem, który przy pomocy zwykłych nagłówków w zapytaniu HTTP informuje przeglądarkę (to przeglądarka pilnuje SOP'a), iż klient, który jest na konkretnym hoście, protokole i porcie może odpytywać serwer o dane. Czyli wracając jednego z przykładów wyżej - http://example.com i http://example.com:8080 - komunikacja będzie możliwa jeżeli serwer (http://example.com:8080) poinformuje przeglądarkę iż ufa klientowi, który znajduje się na http://example.com i ten może odpytywać o dane. Tylko jak to zrobić w aplikacji?
Jak działa mechanizm CORS
Mechanizm CORS ma to do siebie, iż jego nieumiejętne skonfigurowanie powoduje, iż nie działa albo cała aplikacja albo część funkcjonalności - w zależności od tego o czym zapomnimy. Warto również pamiętać, iż obowiązek poprawnej konfiguracji tego mechanizmu leży po stronie programistów piszących cześć serwerową (backend) - frontend nie jest w stanie naprawić błędów, które wynikają ze złej konfiguracji.
Jednak zanim opiszę jakie nagłówki serwer musi wysyłać to jeszcze gwałtownie o dwóch rodzajach zapytań jakie możemy wykonać:
- Simple request
- Preflighted reqest
Idea prostego zapytania (Simple request) jest następująca - jest to każde zapytanie, które spełnia wszystkie poniższe warunki:
- Dozwolone metody: GET, HEAD, POST
- Dozwolone nagłówki (te które możemy sami ustawić): Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width
- Dozwolone wartości dla Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain
Jak wygląda komunikacja klient - serwer w przypadku prostego zapytania:
- Klient wysyła zapytanie do serwera
- Przeglądarka dołącza do zapytania nagłówek Origin
- Serwer zwraca odpowiedź wraz z nagłówkiem Access-Control-Allow-Origin
- Przeglądarka sprawdza czy wartość w Access-Control-Allow-Origin zgadza się z wartością Origin klienta
- Jeśli wartość się nie zgadza to przeglądarka blokuje klienta przed otrzymaniem danych z serwera
Jednak w aplikacjach internetowych nie wykorzystujemy prostych zapytań tylko preflight. Dlaczego? Ponieważ po pierwsze adekwatnie wszystko leci dzięki json'a czyli nie zgadza się Content-Type, po drugie w 90% przypadków mamy jakieś uwierzytelnianie więc przesyłamy dodatkowy nagłówek z autoryzacją.
Jak więc wygląda komunikacja w przypadku preflighted request:
- Klient chce wysłać zapytanie do serwera ale zapytanie nie spełnia założeń simple request
- Zapytanie jest wstrzymywane przez przeglądarkę i jest wysyłane zapytanie preflight, którego celem jest sprawdzenie czy klient może wykonać zamierzone zapytanie
- Zapytanie preflight charakteryzuje się kilkoma elementami:
- wysłane zapytanie ma metodę OPTIONS
- wysyłane są dodatkowe nagłówki Access-Control-Request-Method, Access-Control-Request-Headers oraz ORIGIN (o samych nagłówkach będzie za chwilę)
- W momencie gdy przeglądarka dostaje odpowiedź na zapytanie sprawdza zawartość nagłówków Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers w odpowiedzi
- Jeśli wszystko jest w porządku to jest wysyłane oryginalne zapytanie tak jak w przypadku Simple Request
Nagłówki CORS
Jak widać cały mechanizm CORS opiera się na wysyłaniu odpowiednich nagłówków. Tak więc aby to poprawnie zaimplementować wystarczy, iż będziemy wiedzieć do czego są poszczególne nagłówki oraz będziemy je konsekwentnie wysyłać. Same nagłówki możemy podzielić na dwa rodzaje - te wysyłane od klienta oraz te wysyłane przez serwer. I tak jak nagłówkami od strony klienta nie musimy się przejmować bo ustawia je za nas przeglądarka to o tych od strony serwera musimy pamiętać sami.
Nagłówki po stronie klienta:
- Origin - Orign klienta
- Access-Control-Request-Method - w zapytaniu preflight jest tu wpisywana nazwa metody z jaką klient chciał wykonać zapytanie
- Access-Control-Request-Headers - znajdują się tam nagłówki, które zostaną wykorzystane przy normalnym zapytaniu
Nagłówki po stronie serwera:
- Access-Control-Allow-Origin - serwer zwraca tutaj informacje jaki Origin musi mieć klient by był w stanie wykonać request. Oprócz podania konkretnej wartości można podać tutaj wartość * co oznacza, iż każdy może wykonać zapytanie.
- Access-Control-Allow-Methods - tutaj serwer zwraca jakie rodzaje zapytań są dozwolone
- Access-Control-Allow-Headers - tutaj serwer zwraca informację jakie nagłówki może wysłać klient
Jak sobie poradzić podczas developmentu
Oczywiście prawidłowa konfiguracja dla produkcji nie gwarantuje, iż będzie to działało podczas developmentu. Ja osobiście podczas pisania frontendu nie lubię stawiać u siebie backendu tylko staram się korzystać ze stagingu czy innego testowego środowiska. Ale oczywiście powoduje to problemy z mechanizmem CORS - jak sobie z tym poradzić? Parę sprawdzonych przeze mnie sposobów:
- Zawsze jeżeli mamy więcej niż jedno środowisko testowe możemy poprosić by na tym pobocznym ustawić Access-Control-Allow-Origin na wartość * lub przepisywanie Originu klienta. jeżeli macie tylko jedno środowisko przedprodukcyjne to nigdy nie róbcie takich zabiegów- potem może się okazać iż coś nie działa na produkcji bo jest jakaś błędna konfiguracja
- Wyłączyć ten mechanizm obronny w swojej przeglądarce - można to zrobić albo wtyczką albo uruchomieniem przeglądarki odpowiednimi przełącznikami - rozwiązanie prawie idealne - nie działają wtedy niektóre serwisy np.: Youtube, Google Drive, Twitter a i pewnie jest tego więcej
- No i ostatnia opcja - postawienie sobie lokalnie części serwerowej i zmiana konfiguracji tak aby działała z lokalnym środowiskiem frontendowym
A wy jak sobie radzicie z CORS'em? A może macie jakieś interesujące historie lub use-case'y związane z mechanizmem SOP lub CORS? Zapraszam do komentowania i dzielenia się wpisem.