Cross-Origin Resource Sharing (CORS). Jak to działa?

Aby zrozumieć koncept CORS, należy najpierw zachaczyć o temat Same-origin policy.

Same-origin policy (SOP)

Najprościej mówiąc jest to fundamentalny mechanizm, którego specyfikacja zezwala skryptom i dokumentom na dostęp do innych skryptów i dokumentów, jedynie jeśli są one tego samego pochodzenia. Przez pochodzenie („origin”) rozumiemy jeżeli protokół, host i port są takie same. Mając przykładowy url https://confident.bugspace.pl/posts/myPost, wymiana danych skończy się niepowodzeniem dla min.:

  • http://confident.bugspace.pl/posts/mySecondPost – ponieważ jest użyty inny protokół,
  • https://confident.bugspace.pl:80/posts/myPost – ponieważ używany jest inny port. Dla protokołu https domyślnym portem jest 443,
  • https://secret.bugspace.pl:80/posts/myPost – ponieważ host się zmienił.

Praktyczne przykłady użycia CORS

Zdarzają się przypadki w których chcielibyśmy aby wymiana danych nie była tak rygorystyczna.

Dla przykładu załóżmy, że mamy dwa środowiska – jedno przedprodukcyjne preprod.bugspace.com i drugie testowe tests.bugspace.com. Na pierwszym uruchamiane są testy automatyczne, a drugie służy do ich pisania oraz testów manualnych. Aplikacja, która jest postawiona na obu środowiskach wykorzystuje na froncie REST API. Aby nie stawiać backendu na dwóch serwerach, możemy go postawić na jednym i komunikować się z nim na dwóch środowiskach celem uniknięcia niepotrzebnego zżerania zasobów.

Innym przykładem może być praca frontend developerów. Załóżmy że jest aplikacja, która udostępnia REST API. Aby uniknąć stawiania backendu lokalnie za każdym razem, gdy frontend develpoer zaczyna pracę, można udostępnić środowisko z postawionym samym backendem.

Jak widać na powyższych przykładach, SOP implikuje masę problemów komunikacyjnych. Z pomocą przychodzi nam Cross-Origin Resource Sharing.

Cross-Origin Resource Sharing

Wprowadzenie

Jak już możemy się domyśleć, CORS pozwala nam na bezpieczne przesyłanie requestów HTTP Cross-Origin. Mechanizm Cross-Origin Resource Sharing dodaje nowe nagłówki HTTP, dzięki którym serwer jest w stanie określić czy żądanie jest uprawinione do odczytu plików. Ponadto, jeśli żądanie może wyrządzić szkody na serwerze, specyfikacja CORS zmusza (np. przeglądarkę) aby wysłała najpierw zapytanie OPTIONS celem sprawdzenia dostępnych żądań, a w przypadku zatwierdzenia go (żądania użytkownika) przez serwer, (przeglądarka) przesyła już docelowy request. Początkowo może to brzmieć strasznie, ale nie jest. Przejdźmy od razu do przykładów w celu rozjaśnienia logiki Cross-Origin Resource Sharing.

Zapytanie proste

Do żądań cross-site używany jest obiekt XMLHttpRequest. Istnieje kilka typów żądań cross-site, a do jednych z nich należą żądania proste. Są one tak nazywane, ponieważ nie używają preflightu CORS. Dlaczego? Ponieważ są na tyle proste (nie wykorzystują np. cookies), że nie potrzebują dodatkowych mechanizmów bezpieczeństa. Czym charakteryzują się proste żądania?

  • Jedna z metod HTTP to:
    • HEAD
    • GET
    • POST
  • Poza nagłówkami ustawionymi automatycznie, nagłówki dozwolone to:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type, przy czym jego wartość może być:
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

Jak wyglądałby typowy przykład komunikacji z użyciem prostego żądania cross-site?

                             ----- 2 ---->  Server 1
       ----- 1 ---->         <---- 3 -----
Client               Browser
       <---- 6 -----         ----- 4 ---->  Server 2
                             <---- 5 -----

Na diagramie powyżej, komunikacja wygląda w sposób następujący:

  1. Klient otwiera stronę przez przeglądarkę
  2. Przeglądarka przesyła GET request na Server 1
  3. Server 1 zwraca zawartość strony
  4. W kodzie strony znajduje się żądanie cross-origin (przykładowy kod poniżej), przez co przeglądarka wykonuje zapytanie na Server 2. Załącza ona także nagłówek Origin (w którym wskazuje skąd pochodzi request, w tym przypadku to Server 1), który jest obowiązkowy przy requestach typu cross-origin
  5. Server 2 sprawdza czy Server 1 jest zaufany i w pozytywnym przypadku zwraca mu dane, o które zapytuje
  6. Przeglądarka dostaje odpowiedź, weryfikuje ustawione nagłówki i w razie sukcesu wyświetla stronę klientowi

Poniżej znajduje się przykładowy kod służący do żądania cross-origin. Najpierw następuje inicjalizacja instancji XMLHttpRequest, a następnie przesyłany jest zapytanie GET na zdefiniowany url.

const xhr = new XMLHttpRequest();
const url = 'https://bugspace.pl/data/wanted-data';

xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();

To, czy Server 2 zaakceptuje nasz request jest ustalane za pomocą nagłwków typu Access-Control. Do ich listy przejdziemy na końcu artykułu.

Zapytanie nie-proste

W przypadku zapytań nie-prostych nie ma większej filozofii. Zaczniemy od prostego przykładu.

                             ----- 2 ---->  Server 1
       ----- 1 ---->         <---- 3 -----
Client               Browser
       <---- 8 -----         ----- 4 ---->
                             <---- 5 -----  Server 2
                             ----- 6 ---->
                             <---- 7 -----
                                                
  1. Klient otwiera stronę przez przeglądarkę
  2. Przeglądarka przesyła GET request na Server 1
  3. Server 1 zwraca zawartość strony
  4. Przeglądarka wykonuje zapytanie preflight, którego metoda to OPTIONS z ustawionymi, wymaganymi nagłówkami Origin i Access-Control-Request-Method. Zapytanie sprawdza, czy Server 1 obsługuje żądanie cross-origin o danej metodzie oraz nagłówkach (jeśli takowe są podane)
  5. Server 2 odpowiada na zapytanie preflight, a w responsie załącza obligatoryjne nagłówki Access-Control-Request-Methods oraz Access-Control-Request-Origin.
  6. Przeglądarka dostaje odpowiedź i w razie sukcesu i jej prawidłowości, wykonuje ona docelowe zapytanie na Server 2
  7. Server 2 zwraca dane, ponieważ wcześniej potwierdził, że Server 1 jest zaufany
  8. Przeglądarka dostaje odpowiedź, weryfikuje ustawione nagłówki i w razie sukcesu wyświetla stronę klientowi

Przykładowe żądanie preflight (punkt 4) mogłoby wyglądać następująco:

OPTIONS /posts/firstPost
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://bugspace.pl

Jeśli serwer uzna, że logika zapytania się zgadza (punkt 5), odpowiada w sposób następujący:

HTTP/1.1 204 No Content
Connection: keep-alive
Access-Control-Allow-Origin: https://bugspace.pl
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

Nagłówki odpowiedzi HTTP

Jak wyżej wspomnieliśmy, do żądań i odpowiedzi załączane są nagłówki. Ich lista znajduje się poniżej.

Originwymagane zawsze
Access-Control-Request-Methodwymagane dla zapytań preflight
Access-Control-Request-Headersopcjonalne
Nagłówki zapytań
Access-Control-Allow-Originwymagane zawsze
Access-Control-Expose-Headersopcjonalne
Access-Control-Max-Ageopcjonalne
Access-Control-Allow-Credentialsopcjonalne
Access-Control-Allow-Methodswymagane dla zapytań preflight
Access-Control-Allow-Headerswymagane, jeśli wcześniej był użyty nagłówek Access-Control-Request-Headers
Nagłówki odpowiedzi

Oczywiście, każdy z nagłówków ma swoje specyfikacje, od których zależy, czy zostanie dodany do zapytania. Więcej o nich dowiecie się stąd.

Źródła

https://developer.mozilla.org/pl/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin
https://sekurak.pl/czym-jest-cors-cross-origin-resource-sharing-i-jak-wplywa-na-bezpieczenstwo/
https://medium.com/@baphemot/understanding-cors-18ad6b478e2b

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *