Ovaj članak objašnjava kako omogućiti CORS (Cross-Origin Resource Sharing) sa HTTPOnly kolačićem radi osiguranja pristupnih tokena.
Danas se back-end serveri i front-end klijenti nalaze na različitim domenima. Zbog toga je neophodno da server omogući CORS kako bi klijenti mogli da komuniciraju sa serverom unutar pretraživača.
Serveri takođe implementiraju autentifikaciju bez stanja radi bolje skalabilnosti. Tokeni se čuvaju i održavaju na strani klijenta, a ne na strani servera kao kod sesija. Iz sigurnosnih razloga, preporučuje se čuvanje tokena u HTTPOnly kolačićima.
Zašto su zahtevi sa različitog porekla blokirani?
Pretpostavimo da je naša front-end aplikacija postavljena na https://app.primer.com
. Skripta učitana na https://app.primer.com
može da zahteva samo resurse sa istog porekla.
Kad god pokušamo da pošaljemo zahtev sa drugog porekla na drugi domen https://api.primer.com
, drugi port https://app.primer.com:3000
ili drugu šemu http://app.primer.com
, pretraživač će blokirati takav zahtev.
Međutim, isti zahtev koji pretraživač blokira se šalje bez problema sa bilo kog back-end servera koristeći curl ili alate poput Postmana. Razlog za ovo je sigurnost i zaštita korisnika od napada kao što je CSRF (Cross-Site Request Forgery).
Uzmimo primer: korisnik je prijavljen na svoj PayPal nalog u pretraživaču. Ako bismo mogli da šaljemo zahteve sa drugog porekla na paypal.com
iz skripte učitane na malicious.com
bez CORS greške, to bi bilo isto kao da šaljemo zahtev sa istog porekla.
Napadači bi mogli lako da pošalju svoju zlonamernu stranicu https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account
konvertovanu u skraćeni URL kako bi sakrili stvarni URL. Kada korisnik klikne na zlonamernu vezu, skripta učitana na malicious.com
bi poslala zahtev ka PayPal-u da prenese novac sa korisničkog na napadački PayPal račun. Korisnici koji su prijavljeni na svoj PayPal nalog i kliknu na ovu zlonamernu vezu bi izgubili svoj novac. Svako bi mogao lako ukrasti novac bez znanja korisnika PayPal naloga.
Iz tog razloga, pretraživači blokiraju sve zahteve sa različitog porekla.
Šta je CORS (Cross-Origin Resource Sharing)?
CORS je sigurnosni mehanizam koji koristi zaglavlja kako bi server obavestio pretraživač da dozvoli zahteve sa određenih, pouzdanih domena. Server sa omogućenim CORS zaglavljima pomaže u izbegavanju blokiranja zahteva sa drugog porekla od strane pretraživača.
Kako CORS funkcioniše?
Server definiše pouzdane domene u svojoj CORS konfiguraciji. Kada pošaljemo zahtev serveru, odgovor informiše pretraživač da li je traženi domen pouzdan ili ne, kroz zaglavlja.
Postoje dve vrste CORS zahteva:
- Jednostavan zahtev
- Zahtev pre leta
Jednostavan zahtev:
- Pretraživač šalje zahtev domenu drugog porekla sa informacijom o poreklu (
https://app.primer.com
). - Server vraća odgovor sa dozvoljenim metodama i dozvoljenim poreklom.
- Kada primi odgovor, pretraživač proverava da li se poslata vrednost zaglavlja porekla (
https://app.primer.com
) i primljena vrednost za kontrolu pristupa (https://app.primer.com
) podudaraju ili sadrže džoker.
U suprotnom, izbaciće CORS grešku.
- Zahtev pre leta:
- U zavisnosti od prilagođenih parametara zahteva sa različitog porekla, kao što su metode (PUT, DELETE), prilagođena zaglavlja, ili različiti tipovi sadržaja, pretraživač će prvo poslati OPTIONS zahtev pre stvarnog zahteva, kako bi proverio da li je stvarni zahtev siguran za slanje.
Nakon što dobije odgovor (kod statusa 204, što znači da nema sadržaja), pretraživač proverava parametre kontrole pristupa za stvarni zahtev. Ako server dozvoljava parametre zahteva, stvarni zahtev sa drugog porekla se šalje i prima.
Ako je access-control-allow-origin: *
, onda je odgovor dozvoljen za sva porekla, ali to nije bezbedno, osim ako nije neophodno.
Kako omogućiti CORS?
Za omogućavanje CORS za bilo koji domen, potrebno je podesiti CORS zaglavlja, dozvoljavajući poreklo, metode, prilagođena zaglavlja, akreditive itd.
- Pretraživač čita CORS zaglavlja sa servera i dozvoljava stvarne zahteve od klijenta tek nakon provere parametara zahteva.
Access-Control-Allow-Origin
: Definišite tačne domene (https://app.primer.com
,https://lab.primer.com
) ili koristite džoker*
.Access-Control-Allow-Methods
: Dozvolite HTTP metode (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) koje su vam potrebne.Access-Control-Allow-Headers
: Dozvolite samo određena zaglavlja (Authorization, csrf-token).Access-Control-Allow-Credentials
: Bulova vrednost za omogućavanje akreditiva sa različitog porekla (kolačići, zaglavlje autorizacije).
Access-Control-Max-Age
: Informiše pretraživač da kešira pre-flight odgovor određeno vreme.
Access-Control-Expose-Headers
: Navedite zaglavlja koja su dostupna skripti na strani klijenta.
Za omogućavanje CORS-a na Apache i Nginx veb serverima, pratite odgovarajuće uputstvo.
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') })
Omogućavanje CORS-a u ExpressJS-u
Primer ExpressJS aplikacije bez CORS-a:
npm install cors
U gornjem primeru, omogućili smo API krajnju tačku za korisnike za POST, PUT i GET metode, ali ne i za DELETE.
Za jednostavno omogućavanje CORS-a u ExpressJS aplikaciji, možete instalirati cors:
app.use(cors({ origin: '*' }));
Access-Control-Allow-Origin
app.use(cors({ origin: 'https://app.techblog.co.rs.com' }));
Omogućavanje CORS-a za sve domene
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ] }));
Omogućavanje CORS-a za jedan domen
Ako želite da dozvolite CORS za poreklo https://app.primer.com
i https://lab.primer.com
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'] }));
Access-Control-Allow-Methods
Za omogućavanje CORS-a za sve metode, izostavite ovu opciju u CORS modulu u ExpressJS-u. Za omogućavanje samo određenih metoda (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'] }));
Access-Control-Allow-Headers
Koristi se za omogućavanje slanja zaglavlja osim podrazumevanih sa stvarnim zahtevima.
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 }));
Access-Control-Allow-Credentials
Izostavite ovo ako ne želite da kažete pretraživaču da dozvoli akreditive na zahtev čak i ako je withCredentials
podešeno na 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 }));
Access-Control-Max-Age
Da biste podstakli pretraživač da kešira informacije o odgovoru pre leta u keš memoriju tokom određenog broja sekundi. Izostavite ovo ako ne želite da keširate odgovor.
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'] }));
Keširani odgovor pre objavljivanja biće dostupan 10 minuta u pretraživaču.
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', ] }));
Access-Control-Expose-Headers
Ako postavimo džoker znak *
u exposedHeaders
, neće otkriti zaglavlje autorizacije. Zbog toga, moramo eksplicitno da ga izložimo kao što je prikazano ispod.
Gore navedeno će otkriti sva zaglavlja i zaglavlje autorizacije.
- Šta je HTTP kolačić?
- Kolačić je mali deo podataka koji server šalje pretraživaču klijenta. U kasnijim zahtevima, pretraživač će poslati sve kolačiće koji se odnose na isti domen uz svaki zahtev.
- Kolačić ima svoje atribute koji se mogu definisati kako bi kolačić radio drugačije, u zavisnosti od potrebe.
- Ime: Naziv kolačića.
- Vrednost: Podaci kolačića koji se odnose na naziv kolačića.
- Domen: Kolačići će biti poslati samo na definisani domen.
- Put: Kolačići se šalju samo za definisanu putanju URL adrese. Ako smo definisali put kolačića kao
path='/admin/'
, kolačići se neće slati za URLhttps://primer.com/expire/
, ali će se slati sa URL prefiksomhttps://primer.com/admin/
. - Max-Age/Expires (broj u sekundama): Kada kolačić treba da istekne. Životni vek kolačića ga čini nevažećim nakon navedenog vremena.
- HTTPOnly (Boolean): Back-end server može pristupiti HTTPOnly kolačiću, ali ne i skripta na strani klijenta kada je postavljeno na true.
- Secure (Boolean): Kolačići se šalju samo preko SSL/TLS domena kada je postavljeno na true.
- sameSite (String): Koristi se za omogućavanje/ograničavanje slanja kolačića pri zahtevima sa različite lokacije. Za više detalja o sameSite kolačićima, pogledajte MDN. Prihvata tri opcije: Strict, Lax, None. Bezbedna vrednost kolačića mora biti postavljena na true za konfiguraciju kolačića
sameSite=None
.
Zašto HTTPOnly kolačić za tokene?
Čuvanje pristupnog tokena poslatog sa servera u skladištu na strani klijenta (kao što je lokalna memorija, indexedDB ili kolačić koji nije HTTPOnly) je ranjivije na XSS napade. Ako je neka od vaših stranica ranjiva na XSS napad, napadači mogu zloupotrebiti korisničke tokene sačuvane u pretraživaču.
HTTPOnly kolačići se postavljaju i dobijaju samo od strane servera, a ne na strani klijenta.
- Skripta na strani klijenta je ograničena na pristup HTTPOnly kolačiću. Zbog toga, HTTPOnly kolačići nisu ranjivi na XSS napade i sigurniji su, jer su dostupni samo serveru.
- Omogućite HTTPOnly kolačić na back-endu sa omogućenim CORS-om.
- Za omogućavanje kolačića u CORS-u, potrebna je sledeća konfiguracija u aplikaciji/serveru:
- Postavite zaglavlje
Access-Control-Allow-Credentials
na true.
Access-Control-Allow-Origin
i Access-Control-Allow-Headers
ne smeju da budu džoker.
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') });
.
Atribut sameSite
kolačića treba da bude None
.
Da biste omogućili vrednost sameSite
na None
, podesite bezbednu vrednost na true: Omogućite back-end sa SSL/TLS sertifikatom da radi u imenu domena.
Pogledajmo primer koda koji postavlja pristupni token u HTTPOnly kolačić nakon provere akreditiva za prijavu.
Možete konfigurisati CORS i HTTPOnly kolačiće primenom gore navedena četiri koraka u svom back-end jeziku i veb serveru.
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://api.techblog.co.rs.com/user', true); xhr.withCredentials = true; xhr.send(null);
Možete pratiti uputstva za Apache i Nginx za omogućavanje CORS-a, prateći gore navedene korake.
fetch('http://api.techblog.co.rs.com/user', { credentials: 'include' });
withCredentials
za Cross-Origin zahtev
$.ajax({ url: 'http://api.techblog.co.rs.com/user', xhrFields: { withCredentials: true } });
Akreditivi (kolačić, autorizacija) se podrazumevano šalju sa zahtevom istog porekla. Za zahteve sa drugog porekla, moramo navesti withCredentials
na true.
axios.defaults.withCredentials = true
XMLHttpRequest API
Fetch API
jQuery Ajax Axios Zaključak Nadam se da će vam ovaj članak pomoći da razumete kako CORS funkcioniše i kako omogućiti CORS za zahteve sa drugog porekla na serveru. Takođe, zašto je čuvanje kolačića u HTTPOnly sigurnije, i kako se withCredentials
koristi u klijentima za zahteve sa drugog porekla.