Cross-Origin Resource Sharing to mechanizm, który decyduje, kiedy kod w przeglądarce może bezpiecznie odczytać odpowiedź z innego originu, a kiedy powinien zostać zatrzymany. W praktyce chodzi o konfigurację frontendów, API, fontów, canvasu i integracji z zewnętrznymi usługami, więc temat szybko wychodzi poza teorię. Poniżej rozkładam go na działanie przeglądarki, najważniejsze nagłówki, typowe błędy i zasady, które naprawdę pomagają w produkcji.
Najkrócej, CORS to kontrola dostępu między originami
- CORS działa po stronie przeglądarki i opiera się na nagłówkach HTTP.
- Origin to nie tylko domena, ale zestaw: protokół, host i port.
- Istnieją dwa główne tryby: proste żądania i preflight OPTIONS.
- Access-Control-Allow-Origin jest kluczowy, ale sam nie wystarcza do ochrony API.
- Zbyt szeroki wildcard i odbijanie nagłówka Origin bez walidacji to częste błędy.
- CORS nie zastępuje CSRF, autoryzacji ani sensownej architektury backendu.
Czym jest CORS i dlaczego przeglądarka w ogóle go wymusza
Ja patrzę na CORS jako na kontrolowany wyjątek od zasady same-origin policy. Ta zasada mówi, że dokument lub skrypt załadowany z jednego originu nie powinien swobodnie czytać danych z innego. Origin jest tutaj definiowany bardzo konkretnie: liczy się protokół, host i port, więc https, subdomena albo inny port od razu tworzą osobny kontekst bezpieczeństwa.
To ważne, bo bez takiej blokady dowolny złośliwy skrypt mógłby próbować odczytywać odpowiedzi z cudzych aplikacji, poczty czy paneli administracyjnych. CORS nie znosi tej ochrony, tylko pozwala serwerowi powiedzieć: „tym originom ufam, tym nie”. Właśnie dlatego mechanizm jest tak istotny przy API, które ma działać w przeglądarce, a jednocześnie nie może otwierać się na wszystkich.
W praktyce przeglądarka ogranicza głównie odczyt odpowiedzi. To rozróżnienie ma znaczenie, bo wiele osób myli blokadę CORS z całkowitym zakazem wysłania żądania. To nie tak działa, a różnica wróci jeszcze w dalszej części tekstu. Najpierw warto zobaczyć, jak sam przepływ wygląda krok po kroku.
Jak przebiega żądanie między różnymi originami
W codziennej pracy spotykam dwa scenariusze. Pierwszy to proste żądanie, które przechodzi bez dodatkowej negocjacji. Drugi to preflight, czyli wstępne zapytanie OPTIONS, którym przeglądarka sprawdza, czy właściwy request będzie dozwolony. Jeśli nie rozróżnisz tych ścieżek, można godzinami poprawiać zły nagłówek.
| Scenariusz | Co robi przeglądarka | Co musi zrobić serwer | Kiedy to zwykle występuje |
|---|---|---|---|
| Proste żądanie | Wysyła request bez dodatkowego OPTIONS | Odpowiada poprawnym Access-Control-Allow-Origin | Najczęściej GET, HEAD albo prosty POST |
| Preflight | Najpierw pyta przez OPTIONS, czy wolno wykonać właściwy request | Odpowiada m.in. Access-Control-Allow-Methods i Access-Control-Allow-Headers | Przy niestandardowych metodach, nagłówkach lub bardziej złożonych żądaniach |
W praktyce preflight pojawia się wtedy, gdy request wychodzi poza prosty zestaw metod i nagłówków. Przeglądarka wysyła wtedy dodatkowe informacje, czego chce użyć, a serwer decyduje, czy to akceptuje. Jeśli odpowiedź nie spełnia warunków, JavaScript zwykle dostaje ogólny błąd sieciowy, a szczegóły trzeba sprawdzić w konsoli przeglądarki. To ma chronić użytkownika, ale dla developera bywa frustrujące, bo sygnał jest celowo mało gadatliwy.
Tu zaczynają się nagłówki, które naprawdę decydują o wyniku rozmowy między frontendem a backendem.
Nagłówki, które decydują o tym, czy dostęp przejdzie
Technicznie CORS nie jest jednym nagłówkiem, tylko zestawem reguł zapisanych w HTTP. Najczęściej analizuję je w dwóch grupach: te, które wysyła przeglądarka, i te, które zwraca serwer. Jak opisuje MDN, to właśnie odpowiednie nagłówki mówią przeglądarce, czy może dopuścić odczyt danych z innego originu.
| Strona | Nagłówek | Po co jest | Na co uważać |
|---|---|---|---|
| Żądanie | Origin | Pokazuje, z jakiego originu pochodzi request | Jest zawsze wysyłany przez przeglądarkę, ale nie traktuję go jako samodzielnego mechanizmu autoryzacji |
| Żądanie | Access-Control-Request-Method | Informuje serwer, jakiej metody chce użyć przeglądarka w preflight | Pomaga odróżnić zwykłe żądanie od negocjacji dostępu |
| Żądanie | Access-Control-Request-Headers | Lista nagłówków, które mają pojawić się w właściwym requestcie | Jeśli nie odpowiesz na nie poprawnie, preflight się wywali |
| Odpowiedź | Access-Control-Allow-Origin | Określa, który origin może czytać odpowiedź | * zostawiam tylko dla publicznych, anonimowych zasobów |
| Odpowiedź | Access-Control-Allow-Credentials | Pozwala czytać odpowiedź, gdy w grę wchodzą dane uwierzytelniające | Nie działa razem z Access-Control-Allow-Origin: * |
| Odpowiedź | Access-Control-Allow-Methods | Określa, które metody są dozwolone po preflight | Nie dawaj tu metod, których aplikacja realnie nie obsługuje |
| Odpowiedź | Access-Control-Allow-Headers | Wskazuje, jakie nagłówki może wysłać klient | Za szeroka lista otwiera niepotrzebny margines ryzyka |
| Odpowiedź | Access-Control-Expose-Headers | Mówi, które nagłówki odpowiedzi są widoczne w JS | Przydaje się, gdy frontend ma czytać własne nagłówki metadanych |
| Odpowiedź | Access-Control-Max-Age | Określa, jak długo przeglądarka może cache’ować wynik preflight | Pomaga wydajności, ale utrudnia szybkie zmiany polityki |
Jeśli dynamicznie dopasowujesz origin po stronie serwera, dodaj też Vary: Origin. Bez tego cache albo CDN może podać odpowiedź przewidzianą dla innego klienta. To nie jest detal kosmetyczny, tylko element, który potrafi zepsuć poprawnie wyglądającą konfigurację.
Gdy rozumie się już nagłówki, łatwiej zauważyć błędy, które z pozoru wyglądają niewinnie, a w praktyce robią z CORS lukę bezpieczeństwa.
Najczęstsze błędy, które zamieniają CORS w problem bezpieczeństwa
OWASP zwraca uwagę na dwa powtarzające się problemy: zbyt szeroki wildcard i bezrefleksyjne odbijanie wartości Origin. Ja dodałbym jeszcze trzeci: przekonanie, że skoro CORS jest ustawiony, to API jest już bezpieczne. To wygodne myślenie, ale po prostu błędne.
Zbyt szeroki wildcard
Access-Control-Allow-Origin: * jest akceptowalne dla zasobów publicznych, które nie ujawniają nic wrażliwego. Jeśli jednak odpowiedź zawiera dane użytkownika, tokeny, szczegóły konta albo metadane o sesji, taki wildcard jest zbyt luźny. Szczególnie niebezpieczne robi się to wtedy, gdy ktoś zakłada, że dostęp i tak ogranicza firewall albo adres IP.
Odbijanie originu bez walidacji
Błąd klasyczny: backend bierze wartość z nagłówka Origin i odsyła ją z powrotem, bez dokładnej walidacji. Brzmi jak elastyczne rozwiązanie, ale wystarczy źle napisany warunek albo zbyt luźne dopasowanie domeny, żeby dopuścić obcy serwis. W praktyce to jeden z najczęstszych powodów wycieków danych przez źle skonfigurowane API.
Null origin i środowiska lokalne
W testach i lokalnym developmentcie czasem kusi, żeby dopuścić null jako zaufany origin. To bywa wygodne, ale jest ryzykowne, bo taki przypadek może pojawić się w zaskakujących sytuacjach i zostać wykorzystany przez atakującego. Jeśli musisz to dopuścić, rób to świadomie, krótkoterminowo i tylko tam, gdzie rozumiesz konsekwencje.
Przeczytaj również: Proces ETL - Jak skutecznie integrować dane i unikać błędów?
Credentialed requests bez kontroli
Jeśli odpowiedź ma być czytana razem z ciasteczkami lub innymi danymi uwierzytelniającymi, konfiguracja musi być jeszcze bardziej restrykcyjna. Nie wolno łączyć Access-Control-Allow-Credentials: true z wildcardem w Access-Control-Allow-Origin, a sam fakt, że przeglądarka „przepuszcza” request, nie oznacza jeszcze, że backend powinien go obsłużyć. Przy danych wrażliwych zawsze wolę małą, jawnie utrzymywaną allowlistę niż automatyczne dopasowanie wszystkiego, co wygląda podobnie.
Te błędy pokazują, że CORS trzeba traktować jak część polityki dostępu, a nie szybki przełącznik. Właśnie dlatego następny krok to bezpieczna konfiguracja, a nie tylko „naprawa błędu w konsoli”.
Jak ustawić bezpieczną politykę w API i frontendzie
Najbezpieczniej patrzę na CORS jak na precyzyjną umowę między aplikacją a serwerem. Nie kopiuję ustawień z pierwszego lepszego poradnika, tylko dopasowuję je do tego, czy endpoint jest publiczny, czy wymaga logowania, czy działa za CDN-em i czy frontend naprawdę musi czytać odpowiedź z innej domeny.
- Używaj jawnej allowlisty originów zamiast szerokiego dopasowania „na oko”.
- Rozdziel konfigurację dla dev, stage i prod, bo to trzy różne ryzyka.
- Przy odpowiedziach uwierzytelnionych nie używaj * jako wygodnego skrótu.
- Jeśli origin jest wyliczany dynamicznie, zawsze dodawaj Vary: Origin.
- Ogranicz metody i nagłówki do tego, czego aplikacja rzeczywiście potrzebuje.
- Jeśli frontend i backend mogą działać pod jednym originem lub za reverse proxy, często upraszcza to całą układankę bardziej niż rozbudowane wyjątki CORS.
W praktyce oddzielam też publiczne endpointy od tych, które zwracają dane konta, historię zamówień czy ustawienia użytkownika. Publiczny feed może mieć luźniejsze zasady, ale panel po zalogowaniu już nie. To proste rozdzielenie zwykle daje więcej niż próbę stworzenia jednej „uniwersalnej” polityki dla wszystkiego.
I tu dochodzimy do częstego nieporozumienia: CORS nie jest ani CSRF, ani reverse proxy, ani mechanizmem komunikacji między oknami. To osobny element układanki, który łatwo pomylić z innymi.
Czego CORS nie załatwia i z czym łatwo go pomylić
Ja najczęściej tłumaczę to tak: CORS decyduje o tym, czy JavaScript w przeglądarce może odczytać odpowiedź z innego originu. Nie decyduje natomiast o tym, czy żądanie w ogóle zostanie wysłane. Formularz HTML może wysłać request między originami, obrazek może zostać pobrany, a iframe może załadować treść. Różnica jest subtelna, ale dla bezpieczeństwa ogromna.
| Mechanizm | Co robi | Czego nie robi |
|---|---|---|
| CORS | Kontroluje, czy przeglądarka pozwala skryptowi odczytać odpowiedź z innego originu | Nie zastępuje autoryzacji ani nie blokuje samego wysłania requestu |
| CSRF token | Chroni przed niechcianymi żądaniami zmieniającymi stan aplikacji | Nie rozwiązuje problemu odczytu odpowiedzi przez obcy origin |
| Reverse proxy | Pośredniczy po stronie serwera i może ukryć różnice originów przed przeglądarką | Nie jest automatycznym zabezpieczeniem, jeśli backend dalej ma złą politykę |
| window.postMessage | Umożliwia komunikację między oknami i iframe’ami | Nie służy do pobierania danych z API ani do obchodzenia same-origin policy |
To rozróżnienie jest ważne, bo źle ustawiony CORS często bywa tylko objawem szerszego problemu architektonicznego. Jeśli endpoint zmienia stan, potrzebujesz dodatkowej ochrony przed CSRF. Jeśli integrujesz się z zewnętrzną usługą, czasem lepszy jest backendowy pośrednik niż walka z przeglądarką. A jeśli komunikujesz dwa fronty w iframe, właściwym narzędziem jest postMessage, nie próba „odkręcenia” CORS.
Kiedy to wszystko składasz razem, konfiguracja przestaje być zagadką, a staje się zestawem kilku konkretnych decyzji do sprawdzenia przed wdrożeniem.
Co sprawdzam przed wdrożeniem, żeby CORS nie wrócił jako incydent
Przed publikacją API robię krótką kontrolę, bo większość problemów da się wyłapać szybciej niż po pierwszym zgłoszeniu od użytkownika. To nie jest długa lista, ale każda z tych rzeczy realnie zmniejsza ryzyko błędnej konfiguracji.
- Czy originy są wpisane jawnie i nie ma w nich „domyślnie wszystko”.
- Czy odpowiedzi z danymi wrażliwymi nie używają *.
- Czy przy sesjach i cookies origin jest walidowany dokładnie, a nie „mniej więcej”.
- Czy preflight dopuszcza tylko te metody i nagłówki, które są naprawdę potrzebne.
- Czy odpowiedzi dynamiczne mają Vary: Origin.
- Czy zespół wie, że błąd CORS w przeglądarce nie oznacza automatycznie awarii backendu, tylko problem polityki dostępu.
Jeśli w konsoli widzisz błąd CORS, zaczynam od porównania Origin, nagłówków odpowiedzi i tego, czy żądanie w ogóle wymaga preflightu. W dziewięciu przypadkach na dziesięć problem nie leży w samym fetchu, tylko w zbyt luźnej albo niespójnej polityce po stronie serwera. To właśnie ta warstwa robi największą różnicę między aplikacją, która „jakoś działa”, a taką, która jest przewidywalna i bezpieczna.