CORS zaczyna się od polityki tego samego źródła
Polityka tego samego źródła, czyli same-origin policy, jest podstawowym mechanizmem bezpieczeństwa przeglądarki. Ogranicza możliwość odczytywania przez dokument lub skrypt danych pochodzących z innego originu. Jej celem jest między innymi uniemożliwienie złośliwej stronie odczytania danych z serwisu, w którym użytkownik jest zalogowany. [3]
CORS, czyli Cross-Origin Resource Sharing, jest mechanizmem opartym na HTTP, który pozwala serwerowi poluzować tę regułę i jawnie wskazać, które originy mogą odczytywać daną odpowiedź. Fetch Standard opisuje CORS jako protokół wymagający jawnej zgody serwera. [1][2]
Co dokładnie oznacza origin
| Strona | Adres docelowy | Relacja | Dlaczego |
|---|---|---|---|
| https://app.example.com | https://app.example.com/api | Ten sam origin | Ten sam schemat, host i port |
| https://app.example.com | https://api.example.com | Inny origin | Inny host |
| https://app.example.com | http://app.example.com | Inny origin | Inny schemat |
| https://app.example.com | https://app.example.com:8443 | Inny origin | Inny port |
| https://app.example.com | https://app.example.com/v2 | Ten sam origin | Zmienia się tylko ścieżka |
Origin tworzą trzy elementy: schemat, host i port. Dwa adresy mają ten sam origin wyłącznie wtedy, gdy wszystkie trzy wartości są zgodne. Sama ścieżka URL nie ma znaczenia. [3][4]
Dlatego `https://app.example.com` i `https://api.example.com` mają różne originy mimo wspólnej domeny bazowej. Również `http://app.example.com` i `https://app.example.com` są różne z powodu schematu, a porty 443 i 8443 tworzą różne originy. [4]
Pojęcia same-origin i same-site nie są równoważne. Ma to znaczenie szczególnie przy cookies, gdzie atrybut `SameSite` działa według pojęcia site, a CORS według originu. [13]
Co naprawdę znaczy komunikat, że przeglądarka blokuje API
Sformułowanie, że przeglądarka blokuje żądanie, jest często zbyt dużym uproszczeniem. Dla prostych żądań cross-origin przeglądarka może wysłać żądanie do serwera, otrzymać poprawną odpowiedź HTTP, a następnie nie udostępnić tej odpowiedzi kodowi JavaScript, jeżeli kontrola CORS zakończy się niepowodzeniem. [1][6]
Dla żądań wymagających preflight sytuacja jest inna. Przeglądarka wysyła najpierw `OPTIONS`. Jeżeli odpowiedź preflight nie spełnia wymagań CORS, właściwe żądanie nie jest wysyłane. [2][5][6]
To rozróżnienie jest ważne przy debugowaniu operacji zmieniających dane. Nie wolno zakładać, że komunikat CORS oznacza, iż backend na pewno nie otrzymał żadnego żądania.
Proste żądania: kiedy przeglądarka pomija preflight
| Metoda | Przykładowe nagłówki | Proste żądanie | Efekt |
|---|---|---|---|
| GET | Accept | Tak | Bez preflight |
| POST | Content-Type: text/plain | Tak | Bez preflight |
| POST | Content-Type: application/json | Nie | JSON powoduje preflight |
| PUT | Content-Type: application/json | Nie | Metoda powoduje preflight |
| GET | Authorization: Bearer ... | Nie | Nagłówek powoduje preflight |
Żądanie jest traktowane jako proste tylko wtedy, gdy spełnia wszystkie warunki bezpiecznej listy CORS. Dozwolone metody to `GET`, `HEAD` i `POST`. Ręcznie ustawiane nagłówki muszą należeć do bezpiecznej listy, a `Content-Type`, jeśli jest ustawiany, może używać tylko `application/x-www-form-urlencoded`, `multipart/form-data` albo `text/plain`. [1]
Typowe `POST` z `Content-Type: application/json` nie jest prostym żądaniem i zwykle uruchamia preflight. Podobnie `PUT`, `DELETE` albo niestandardowy nagłówek, taki jak `X-Request-ID`, powodują konieczność sprawdzenia uprawnień przed właściwym żądaniem. [1][11]
Preflight OPTIONS: co sprawdza przeglądarka
Preflight jest automatycznym żądaniem `OPTIONS`. Przeglądarka dodaje `Origin`, `Access-Control-Request-Method` i, jeśli trzeba, `Access-Control-Request-Headers`. Serwer odpowiada informacją, jakie originy, metody i nagłówki dopuszcza. [2][5]
Jeśli aplikacja chce wysłać `DELETE` z nagłówkiem `Authorization`, przeglądarka może najpierw zapytać serwer, czy dany origin może wykonać `DELETE` i użyć tego nagłówka. Dopiero pozytywna odpowiedź umożliwia wysłanie właściwego żądania. [5]
Preflight tworzy dodatkowy cykl sieciowy, dlatego jego wynik może być przechowywany w specjalnej pamięci podręcznej CORS niezależnej od zwykłej pamięci podręcznej HTTP. [5][10]
Najważniejsze nagłówki CORS
| Nagłówek | Kierunek | Rola |
|---|---|---|
| Origin | Żądanie | Origin inicjujący żądanie |
| Access-Control-Allow-Origin | Odpowiedź | Origin, któremu przeglądarka może udostępnić odpowiedź |
| Access-Control-Allow-Methods | Odpowiedź preflight | Metody dopuszczone przez preflight |
| Access-Control-Allow-Headers | Odpowiedź preflight | Nagłówki żądania dopuszczone przez preflight |
| Access-Control-Allow-Credentials | Odpowiedź | Zgoda na udostępnienie odpowiedzi z danymi uwierzytelniającymi |
| Access-Control-Expose-Headers | Odpowiedź | Dodatkowe nagłówki odpowiedzi dostępne dla JavaScriptu |
| Access-Control-Max-Age | Odpowiedź preflight | Czas przechowywania wyniku preflight |
| Vary: Origin | Odpowiedź | Informacja dla pamięci podręcznej, że odpowiedź zależy od Origin |
`Origin` jest nagłówkiem żądania wskazującym origin inicjujący żądanie. `Access-Control-Allow-Origin` jest nagłówkiem odpowiedzi serwera i mówi przeglądarce, czy odpowiedź może zostać udostępniona kodowi z danego originu. [1][2][8]
`Access-Control-Allow-Methods` i `Access-Control-Allow-Headers` są szczególnie istotne dla preflight. `Access-Control-Allow-Credentials` pozwala udostępnić odpowiedź żądania wykonywanego z danymi uwierzytelniającymi, natomiast `Access-Control-Expose-Headers` może odsłonić JavaScriptowi dodatkowe nagłówki odpowiedzi poza bezpieczną listą. [1][2]
Jeżeli serwer dynamicznie zwraca różne wartości `Access-Control-Allow-Origin` zależnie od żądania, MDN zaleca również `Vary: Origin`, aby pamięć podręczna wiedziała, że odpowiedź zależy od originu. [1]
`Access-Control-Allow-Origin`: `*` czy konkretny origin
`Access-Control-Allow-Origin: *` oznacza, że odpowiedź może zostać udostępniona dowolnemu originowi dla żądań bez danych uwierzytelniających. Dla publicznego API może to być prawidłowa konfiguracja. [2][8]
Dla prywatnych lub wrażliwych punktów końcowych API lepiej dopuszczać minimalny wymagany zestaw originów. MDN i OWASP rekomendują konfigurację możliwie restrykcyjną zamiast bezwarunkowego `*`. [14][17]
Jeżeli aplikacja obsługuje wiele dozwolonych originów, serwer powinien porównać `Origin` z listą dozwolonych originów i zwrócić konkretną zaakceptowaną wartość. Nie należy po prostu odbijać dowolnego `Origin` przesłanego przez klienta. [8][16]
Cookies i dane uwierzytelniające: najczęstsze źródło problemów
W Fetch API można użyć `credentials: "include"`, aby poprosić przeglądarkę o wysyłanie danych uwierzytelniających także w żądaniach cross-origin. Sam parametr po stronie frontendu nie wystarcza. Serwer musi odpowiedzieć między innymi `Access-Control-Allow-Credentials: true` oraz konkretnym `Access-Control-Allow-Origin`. Symbol `*` nie jest wtedy dozwolony. [2][6][9]
Cookies podlegają dodatkowo własnym regułom. `SameSite` może spowodować, że cookie w ogóle nie zostanie wysłane w żądaniu cross-site. Dla `SameSite=None` wymagany jest również `Secure`. [13]
CORS i `SameSite` rozwiązują inne problemy. Można mieć poprawny CORS, ale brak cookie przez `SameSite`, albo poprawnie wysyłane cookie i blokadę odczytu odpowiedzi przez błędny CORS.
Dlaczego `mode: "no-cors"` zwykle niczego nie naprawia
`fetch(..., { mode: "no-cors" })` nie jest sposobem na obejście CORS dla aplikacji, która musi przeczytać odpowiedź API. Takie żądanie ma dodatkowe ograniczenia metod i nagłówków, a odpowiedź jest typu opaque. JavaScript nie może odczytać jej treści ani nagłówków, a kod stanu widoczny przez Fetch API ma wartość `0`. [6][7][11]
`no-cors` ma zastosowania specjalistyczne, między innymi w niektórych scenariuszach Service Worker, ale dla typowego `fetch` pobierającego JSON jest zwykle błędnym rozwiązaniem. [6]
Jeżeli nie kontrolujesz zewnętrznego API i serwer nie udostępnia go przez CORS, rozwiązaniem może być backend lub proxy kontrolowane przez Ciebie, które wykona żądanie serwer-serwer. [11][12]
Dlaczego curl, Postman albo backend działają, a przeglądarka nie
Polityka tego samego źródła i egzekwowanie CORS są mechanizmami bezpieczeństwa przeglądarki. Klient HTTP działający poza tym modelem może wysłać żądanie bez podlegania przeglądarkowej kontroli CORS. Z tego powodu punkt końcowy API może działać w curl lub narzędziu API, a jednocześnie być niedostępny dla JavaScriptu uruchomionego na konkretnej stronie. [2][3][16]
To również pokazuje, dlaczego CORS nie jest uwierzytelnianiem API. Klient spoza przeglądarki może ustawić własny `Origin`, dlatego OWASP ostrzega, aby nie używać tego nagłówka jako dowodu tożsamości klienta. [16]
API nadal potrzebuje normalnych mechanizmów autoryzacji, tokenów, sesji, kontroli uprawnień i walidacji po stronie serwera.
CORS nie zastępuje CSRF ani autoryzacji
Polityka tego samego źródła przede wszystkim ogranicza odczyt danych między originami. Nie oznacza to, że każde żądanie zmieniające stan z innej strony jest automatycznie niemożliwe. Proste formularze i niektóre proste żądania mogą być wysyłane cross-origin, dlatego aplikacja oparta na cookies nadal potrzebuje ochrony CSRF. [3][15]
OWASP wprost zaleca, aby nie polegać wyłącznie na CORS i `Origin` jako kontroli dostępu do wrażliwych danych. Wrażliwe punkty końcowe API wymagają uwierzytelniania i autoryzacji niezależnie od CORS. [16]
CORS odpowiada na pytanie, czy przeglądarka może udostępnić odpowiedź kodowi danego originu. Nie odpowiada na pytanie, czy użytkownik albo klient ma prawo wykonać daną operację.
Najczęstsze błędy i jak je diagnozować
Brak `Access-Control-Allow-Origin` jest jednym z najczęstszych błędów. Inne typowe problemy to niedozwolona metoda, brak wymaganego nagłówka w `Access-Control-Allow-Headers`, użycie `*` z danymi uwierzytelniającymi, błędna odpowiedź na OPTIONS albo przekierowanie podczas przepływu CORS. [11][12]
JavaScript celowo dostaje ograniczoną informację o przyczynie błędu. Dokładniejszy powód należy sprawdzić w konsoli i zakładce Network w DevTools. [1][11]
Komunikat `CORS request did not succeed` nie musi oznaczać błędnej konfiguracji CORS. MDN wskazuje, że może wynikać z DNS, przekroczenia czasu, odmowy połączenia, TLS, mixed content albo blokady przez rozszerzenie. [11]
- Sprawdź dokładny origin frontendu: schemat, host i port.
- Sprawdź żądanie `OPTIONS`, jeśli występuje.
- Zweryfikuj kod stanu odpowiedzi preflight i wszystkie `Access-Control-Allow-*`.
- Sprawdź, czy właściwa odpowiedź także zawiera `Access-Control-Allow-Origin`.
- Przy cookies zweryfikuj `credentials`, `Access-Control-Allow-Credentials`, `SameSite` i `Secure`.
- Sprawdź przekierowania, TLS, mixed content i błędy sieciowe.
- Porównaj żądanie z przeglądarki z działającym żądaniem z curl lub narzędzia API.
Niebezpieczne konfiguracje CORS
Bezwarunkowe odbijanie wartości `Origin` jest niebezpieczne, jeżeli żądania z danymi uwierzytelniającymi zwracają wrażliwe dane. Atakujący może uruchomić własną stronę i spróbować odczytać odpowiedź w kontekście zalogowanego użytkownika, jeżeli jego origin zostanie zaakceptowany. [16]
Podobnie ryzykowne są zbyt szerokie wyrażenia regularne i zaufanie wszystkim subdomenom, jeżeli część z nich może zostać przejęta lub kontrolowana przez inny zespół. OWASP rekomenduje dokładne dopasowanie do listy zaufanych originów. [16][17]
`Access-Control-Allow-Origin: *` nie jest samo w sobie podatnością dla całkowicie publicznych danych bez danych uwierzytelniających. Problem powstaje wtedy, gdy polityka udostępnia więcej danych lub originów niż rzeczywiście powinna. [8][17]
Wydajność: koszt preflight i `Access-Control-Max-Age`
Preflight oznacza dodatkowe żądanie `OPTIONS` przed właściwym żądaniem, więc może zwiększyć opóźnienie. Wynik może być przechowywany za pomocą `Access-Control-Max-Age`. Pamięć preflight jest oddzielna od zwykłej pamięci podręcznej HTTP. [5][10]
MDN podaje, że domyślna wartość bez nagłówka wynosi 5 sekund. Przeglądarki mogą narzucać własne górne limity, nawet jeżeli serwer poda większą wartość. Firefox ogranicza ją do 86400 sekund, a Chromium od wersji 76 do 7200 sekund. [10]
Warto przechowywać stabilne polityki preflight, ale nie kosztem możliwości szybkiego wycofania błędnej lub zbyt szerokiej polityki bezpieczeństwa.
Praktyczna konfiguracja i checklista wdrożeniowa
Najpierw zdecyduj, czy punkt końcowy API w ogóle ma być dostępny cross-origin. Jeśli nie, nie dodawaj nagłówków CORS. Jeśli tak, zdefiniuj dokładną listę dozwolonych originów, potrzebne metody i nagłówki oraz zdecyduj, czy żądania wymagają danych uwierzytelniających. [14][17]
Dla dynamicznej listy dozwolonych originów zwracaj tylko zweryfikowany origin i dodaj `Vary: Origin`. Dla danych uwierzytelniających zwracaj konkretny origin oraz `Access-Control-Allow-Credentials: true`. Nie próbuj naprawiać problemu przez dodawanie nagłówków CORS po stronie frontendu. [1][2]
Po wdrożeniu przetestuj zarówno proste żądanie, jak i preflight, scenariusz z cookies, niedozwolony origin oraz rzeczywiste błędy w DevTools.
- Czy cross-origin jest naprawdę potrzebny?
- Jakie dokładnie originy mają być dozwolone?
- Czy potrzebne są dane uwierzytelniające?
- Jakie metody i nagłówki żądania są wymagane?
- Czy `OPTIONS` dociera do aplikacji i zwraca poprawną odpowiedź?
- Czy właściwa odpowiedź ma poprawne nagłówki CORS?
- Czy przy dynamicznym originie ustawiasz `Vary: Origin`?
- Czy punkt końcowy API nadal wymaga normalnego uwierzytelniania i autoryzacji?
- Czy polityka została przetestowana dla originu dozwolonego i niedozwolonego?

