W tym tekście przyjrzymy się, w jaki sposób aktywować mechanizm CORS (ang. Cross-Origin Resource Sharing) z wykorzystaniem ciasteczek HTTPOnly, co pozwoli nam na skuteczne zabezpieczenie tokenów dostępu.
Obecnie powszechną praktyką jest implementowanie serwerów backendowych i aplikacji frontendowych w odrębnych domenach. Z tego powodu, aby umożliwić komunikację między aplikacją kliencką a serwerem za pośrednictwem przeglądarek, niezbędne jest włączenie mechanizmu CORS po stronie serwera.
Dodatkowo, w celu osiągnięcia lepszej skalowalności, serwery coraz częściej stosują uwierzytelnianie bezstanowe. W takim podejściu tokeny są przechowywane i zarządzane po stronie klienta, a nie po stronie serwera, jak ma to miejsce w przypadku sesji. Ze względów bezpieczeństwa, rekomendowaną praktyką jest przechowywanie tokenów w ciasteczkach HTTPOnly.
Dlaczego żądania pochodzące z różnych źródeł są blokowane?
Rozważmy sytuację, w której nasza aplikacja frontendowa jest dostępna pod adresem https://app.newsblog.pl.com. Skrypt załadowany z tej domeny ma uprawnienia do żądania zasobów tylko z tego samego źródła.
W każdej sytuacji, gdy podejmujemy próbę wysłania żądania do innej domeny, np. https://api.newsblog.pl.com, innego portu, np. https://app.newsblog.pl.com:3000, lub innego schematu, np. http://app.newsblog.pl.com, żądanie to zostanie zablokowane przez przeglądarkę ze względu na mechanizm CORS.
Można zastanawiać się, dlaczego to samo żądanie, które jest blokowane przez przeglądarkę, może być bez problemu wysłane z dowolnego serwera za pomocą narzędzia curl lub innych narzędzi, takich jak Postman. Odpowiedź kryje się w bezpieczeństwie. Ma to na celu ochronę użytkowników przed atakami takimi jak CSRF (Cross-Site Request Forgery).
Przyjrzyjmy się przykładowi. Załóżmy, że użytkownik zalogował się na swoje konto PayPal w przeglądarce. Gdybyśmy mogli wysłać żądanie do paypal.com z poziomu skryptu załadowanego z innej, złośliwej domeny, np. malicious.com, bez jakichkolwiek ograniczeń CORS, podobnie jak w przypadku żądania z tej samej domeny,
osoby atakujące mogłyby bez trudu utworzyć złośliwą stronę, np. https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account, ukrywając prawdziwy adres URL pod skróconym linkiem. W momencie, gdy użytkownik kliknie taki link, złośliwy skrypt wysłałby żądanie do PayPal, zlecając przelew środków z konta użytkownika na konto osoby atakującej. Wszyscy użytkownicy zalogowani na PayPal i klikający w ten link, straciliby swoje pieniądze. W ten sposób, bez wiedzy użytkownika, można łatwo dokonać kradzieży.
Właśnie dlatego przeglądarki domyślnie blokują wszystkie żądania pochodzące z różnych źródeł.
Czym jest CORS (udostępnianie zasobów między źródłami)?
CORS to mechanizm bezpieczeństwa oparty na nagłówkach HTTP. Używany jest przez serwer do informowania przeglądarki, z jakich zaufanych domen mogą być wysyłane żądania pochodzące z innych źródeł.
Serwer, który posiada włączone nagłówki CORS, skutecznie zapobiega blokowaniu żądań między źródłami przez przeglądarki.
Jak działa mechanizm CORS?
Serwer definiuje zaufane domeny w swojej konfiguracji CORS. Gdy przeglądarka wysyła żądanie, serwer w odpowiedzi informuje przeglądarkę, czy domena, z której pochodzi żądanie, jest zaufana, czy też nie.
Istnieją dwa typy żądań CORS:
- Żądanie proste
- Żądanie poprzedzone zapytaniem wstępnym
Żądanie proste:
- Przeglądarka wysyła żądanie do domeny z innego źródła, wraz z informacją o pochodzeniu (np. https://app.newsblog.pl.com).
- Serwer odsyła odpowiedź zawierającą informacje o dozwolonych metodach i dozwolonym pochodzeniu.
- Po otrzymaniu odpowiedzi, przeglądarka weryfikuje, czy przesłana wartość nagłówka „Origin” (np. https://app.newsblog.pl.com) jest identyczna z wartością „Access-Control-Allow-Origin” (np. https://app.newsblog.pl.com) lub czy pasuje do symbolu wieloznacznego. W przypadku niezgodności, przeglądarka zgłosi błąd CORS.
- Żądanie poprzedzone zapytaniem wstępnym:
- W sytuacji, gdy żądanie do domeny z innego źródła zawiera niestandardowe parametry, takie jak metody (PUT, DELETE), niestandardowe nagłówki, czy też inne typy zawartości, przeglądarka wysyła wstępne żądanie OPTIONS. Celem jest sprawdzenie, czy rzeczywiste żądanie może być bezpiecznie wysłane.
Po uzyskaniu odpowiedzi (kod stanu: 204, oznaczający brak treści), przeglądarka analizuje parametry „Access-Control-Allow” dla faktycznego żądania, upewniając się, czy są one dozwolone przez serwer. Następnie, wysyłane jest właściwe żądanie.
Jeśli „Access-Control-Allow-Origin” ma wartość „*”, odpowiedź serwera jest dostępna dla każdego źródła. Jest to jednak niebezpieczne rozwiązanie, chyba że jest to absolutnie konieczne.
Jak włączyć mechanizm CORS?
Aby aktywować CORS dla określonej domeny, konieczne jest włączenie nagłówków CORS, które umożliwiają zdefiniowanie dozwolonych źródeł, metod, niestandardowych nagłówków oraz poświadczeń.
- Przeglądarka analizuje nagłówki CORS otrzymane od serwera i dopuszcza właściwe żądania od klienta po uprzedniej weryfikacji parametrów.
- Access-Control-Allow-Origin: Służy do definiowania konkretnych domen (np. https://app.geekflate.com, https://lab.newsblog.pl.com) lub stosowania symbolu wieloznacznego.
- Access-Control-Allow-Methods: Umożliwia zdefiniowanie dozwolonych metod HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS).
- Access-Control-Allow-Headers: Określa, jakie niestandardowe nagłówki (np. authorization, csrf-token) są dozwolone.
- Access-Control-Allow-Credentials: Wartość logiczna, umożliwia przekazywanie poświadczeń między źródłami (np. pliki cookie, nagłówek Authorization).
Access-Control-Max-Age: Informuje przeglądarkę, jak długo ma buforować odpowiedź na wstępne żądanie.
Access-Control-Expose-Headers: Umożliwia określenie nagłówków, które mają być dostępne dla skryptu po stronie klienta.
Aby skonfigurować mechanizm CORS na serwerach Apache i Nginx, skorzystaj z dedykowanych poradników.
const express = require('express'); const app = express() app.get('/users', function (req, res, next) { res.json({msg: 'user get'}) }); app.post('/users', function (req, res, next) { res.json({msg: 'user create'}) }); app.put('/users', function (req, res, next) { res.json({msg: 'User update'}) }); app.listen(80, function () { console.log('CORS-enabled web server listening on port 80') })
Włączanie CORS w ExpressJS
Rozważmy przykładową aplikację ExpressJS bez włączonego CORS:
npm install cors
W powyższym przykładzie, zdefiniowano punkt końcowy API dla użytkowników, obsługujący metody POST, PUT, GET, ale nie DELETE.
Aby w prosty sposób włączyć CORS w aplikacji ExpressJS, zainstaluj bibliotekę „cors”:
app.use(cors({ origin: '*' }));
Kontrola dostępu – Zezwól – Pochodzenie
app.use(cors({ origin: 'https://app.newsblog.pl.com' }));
Włączanie CORS dla wszystkich domen
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ] }));
Włączanie CORS dla jednej domeny
Jeśli chcemy zezwolić na CORS dla źródeł https://app.newsblog.pl.com i https://lab.newsblog.pl.com:
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'] }));
Metody Kontroli Dostępu
Aby włączyć CORS dla wszystkich metod, wystarczy pominąć tę opcję w module CORS ExpressJS. Możliwe jest też włączenie tylko konkretnych metod (GET, POST, PUT).
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'] }));
Kontrola dostępu – Zezwól – Nagłówki
Służy do zezwalania na wysyłanie nagłówków, które nie są domyślne, wraz z faktycznymi żądaniami.
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true }));
Dostęp – Kontrola – Zezwalaj – Poświadczenia
Pomiń tę konfigurację, jeśli nie chcesz, aby przeglądarka przekazywała poświadczenia wraz z żądaniem, nawet gdy ustawiona jest wartość true.
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600 }));
Kontrola dostępu – Maksymalny Wiek
Informuje przeglądarkę, aby przez określony czas buforowała informacje o odpowiedziach na zapytanie wstępne. Pomiń tę opcję, jeśli nie chcesz buforować odpowiedzi.
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['Content-Range', 'X-Content-Range'] }));
Buforowana odpowiedź na zapytanie wstępne będzie dostępna w przeglądarce przez 10 minut.
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['*', 'Authorization', ] }));
Kontrola dostępu – Expose – Nagłówki
Jeśli w „exposedHeaders” użyjemy symbolu wieloznacznego „*”, nie zostanie ujawniony nagłówek „Authorization”. W takim przypadku należy go zdefiniować jawnie, tak jak poniżej.
Powyższa konfiguracja ujawnia wszystkie nagłówki, w tym nagłówek „Authorization”.
- Czym jest plik cookie HTTP?
- Plik cookie to niewielki fragment danych wysyłany przez serwer do przeglądarki klienta. Podczas kolejnych żądań, przeglądarka przesyła wszystkie pliki cookie powiązane z daną domeną.
- Plik cookie posiada atrybuty, które umożliwiają precyzyjne określenie jego działania.
- Nazwa: Nazwa pliku cookie.
- Wartość: Dane przechowywane w pliku cookie, przypisane do jego nazwy.
- Domena: Pliki cookie będą wysyłane tylko dla zdefiniowanej domeny.
- Ścieżka: Pliki cookie wysyłane są wyłącznie dla zdefiniowanej ścieżki URL. Na przykład, jeżeli ustawimy ścieżkę pliku cookie na „path=’admin/'”, pliki cookie nie będą wysyłane dla adresu URL https://newsblog.pl.com/expire/, ale będą przesyłane w przypadku adresu URL https://newsblog.pl.com/admin/.
- Max-Age/Expires (w sekundach): Określa czas, po którym plik cookie wygasa. Pozwala na ustawienie okresu ważności pliku cookie.
- HTTPOnly (Boolean): Jeśli ustawiona jest wartość „true”, dostęp do pliku cookie HTTPOnly ma tylko serwer, a nie skrypty po stronie klienta.
- Secure (Boolean): Jeśli „true”, pliki cookie będą przesyłane wyłącznie przez bezpieczne połączenie SSL/TLS.
- sameSite (String): Używany do kontrolowania, czy pliki cookie są wysyłane w ramach żądań między witrynami. Aby dowiedzieć się więcej o ustawieniach tego parametru, zajrzyj do dokumentacji MDN: SameSite.
Ten parametr akceptuje trzy wartości: Strict, Lax i None. Aby skonfigurować pliki cookie z wartością sameSite=None, atrybut Secure musi być ustawiony na true.
Dlaczego ciasteczko HTTPOnly jest zalecane dla tokenów?
Przechowywanie tokena dostępu, otrzymanego z serwera, w pamięci po stronie klienta (np. Local Storage, IndexedDB lub plik cookie bez flagi HTTPOnly) zwiększa ryzyko ataku XSS. Jeżeli któraś ze stron aplikacji jest podatna na XSS, atakujący może wykorzystać tokeny użytkownika przechowywane w przeglądarce.
Ciasteczka HTTPOnly mogą być ustawiane i odczytywane jedynie przez serwer, a nie przez skrypty po stronie klienta.
- Skrypt po stronie klienta nie ma dostępu do ciasteczka HTTPOnly, co chroni je przed atakami XSS. Jest to bardziej bezpieczne podejście, gdyż dostęp do tych ciasteczek ma tylko serwer.
- Włącz pliki cookie HTTPOnly w backendzie z obsługą CORS.
- Włączenie obsługi plików cookie w CORS wymaga odpowiedniej konfiguracji aplikacji/serwera:
- Ustaw nagłówek „Access-Control-Allow-Credentials” na „true”.
Wartości nagłówków „Access-Control-Allow-Origin” i „Access-Control-Allow-Headers” nie powinny być ustawione na symbol wieloznaczny.
const express = require('express'); const app = express(); const cors = require('cors'); app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['*', 'Authorization' ] })); app.post('/login', function (req, res, next) { res.cookie('access_token', access_token, { expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), //second min hour days year secure: true, // set to true if your using https or samesite is none httpOnly: true, // backend only sameSite: 'none' // set to none for cross-request }); res.json({ msg: 'Login Successfully', access_token }); }); app.listen(80, function () { console.log('CORS-enabled web server listening on port 80') });
.
Atrybut „sameSite” pliku cookie powinien mieć wartość „None”.
Aby włączyć wartość „sameSite” ustawioną na „none”, ustaw atrybut „secure” na „true”. Do poprawnego działania wymagany jest certyfikat SSL/TLS dla backendu.
Przyjrzyjmy się przykładowemu kodowi, który ustawia token dostępu w ciasteczku HTTPOnly po poprawnej weryfikacji danych logowania.
Ciasteczka CORS i HTTPOnly można skonfigurować, wykonując powyższe cztery kroki, zarówno w języku programowania backendu, jak i na serwerze internetowym.
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://api.newsblog.pl.com/user', true); xhr.withCredentials = true; xhr.send(null);
Możesz skorzystać z tego poradnika, aby włączyć CORS na Apache i Nginx, wykonując opisane kroki.
fetch('http://api.newsblog.pl.com/user', { credentials: 'include' });
withCredentials for Cross-Origin request
$.ajax({ url: 'http://api.newsblog.pl.com/user', xhrFields: { withCredentials: true } });
Poświadczenia (plik cookie, autoryzacja) domyślnie wysyłane są w przypadku żądania z tej samej domeny. W przypadku żądania z innej domeny, należy jawnie ustawić parametr „withCredentials” na „true”.
axios.defaults.withCredentials = true
Interfejs API XMLHttpRequest
Pobierz interfejs API
JQuery AjaxAksjosWniosek Mam nadzieję, że niniejszy artykuł pomógł w zrozumieniu mechanizmu CORS oraz w jego aktywacji dla żądań pochodzących z różnych źródeł na serwerze. Ponadto wyjaśnił, dlaczego przechowywanie plików cookie w trybie HTTPOnly jest bezpieczne oraz jak używać poświadczeń w aplikacjach klienckich dla żądań cross-origin.
newsblog.pl
Maciej – redaktor, pasjonat technologii i samozwańczy pogromca błędów w systemie Windows. Zna Linuxa lepiej niż własną lodówkę, a kawa to jego główne źródło zasilania. Pisze, testuje, naprawia – i czasem nawet wyłącza i włącza ponownie. W wolnych chwilach udaje, że odpoczywa, ale i tak kończy z laptopem na kolanach.