Jestliže poÅ¡leme požadavek fetch na jiné webové sÃdlo, pravdÄpodobnÄ neuspÄje.
Zkusme napÅÃklad stáhnout http://example.com:
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // Failed to fetch
}
Jak jsme oÄekávali, staženà selhalo.
KlÃÄovým konceptem je zde původ â trojice doména/port/protokol.
Požadavky jiného (kÅÞového) původu â ty, které jsou posÃlány na jinou doménu (nebo i subdoménu), protokol nebo port â vyžadujà speciálnà hlaviÄky ze vzdálené strany.
Tato politika se nazývá âCORSâ: Cross-Origin Resource Sharing â sdÃlenà zdrojů kÅÞového původu.
ProÄ je CORS zapotÅebÃ? StruÄná historie
CORS existuje proto, aby chránila internet pÅed zlými hackery.
TeÄ vážnÄ. UdÄlejme si kraÅ¥ouÄkou odboÄku do historie.
Mnoho let nemohl skript z jednoho sÃdla pÅistupovat k obsahu z jiného sÃdla.
Toto jednoduché, ale silné pravidlo bylo základem internetové bezpeÄnosti. NapÅÃklad zlý skript z webového sÃdla hacker.com nemohl pÅistupovat k uživatelovÄ poÅ¡tovnà schránce na webovém sÃdle gmail.com. Lidé mÄli pocit bezpeÄÃ.
NavÃc JavaScript v té dobÄ nemÄl žádné speciálnà metody k provádÄnà sÃÅ¥ových požadavků. Byl to jen jazyk âna hranÃâ, sloužÃcà k dekoraci webových stránek.
Jenže vývojáÅi webů požadovali dalšà možnosti. Byla vyvinuta celá Åada triků, jak toto omezenà obejÃt a odesÃlat požadavky na jiná webová sÃdla.
PoužÃvánà formuláÅů
JednÃm způsobem, jak komunikovat s jiným serverem, bylo odeslat na nÄj <form>. Lidé ho vkládali do <iframe>, aby zůstali na aktuálnà stránce, napÅÃklad:
<!-- cÃl formuláÅe -->
<iframe name="iframe"></iframe>
<!-- formuláŠmůže být dynamicky vygenerován a odeslán JavaScriptem -->
<form target="iframe" method="POST" action="http://jiny-server.com/â¦">
...
</form>
I bez sÃÅ¥ových metod tedy bylo možné vytvoÅit požadavek GET/POST na jiné sÃdlo, neboÅ¥ formuláÅe mohou posÃlat data kamkoli. Protože vÅ¡ak je zakázáno pÅistupovat k obsahu <iframe> z jiného sÃdla, nebylo možné naÄÃst odpovÄÄ.
Abychom byli pÅesnÃ, i na to ve skuteÄnosti existovaly triky, které vyžadovaly speciálnà skripty jak ve vnitÅnÃm rámu, tak na stránce. Komunikace s vnitÅnÃm rámem tedy byla technicky možná. Dnes již nemá smysl zabÃhat do detailů, nechme tyto dinosaury v klidu spát.
PoužÃvánà skriptů
DalÅ¡Ãm trikem bylo použità znaÄky script. Skript může mÃt libovolný src, s libovolnou doménou, napÅÃklad <script src="http://jiny-server.com/â¦">. Je možné spustit skript z libovolného webového sÃdla.
Jestliže webové sÃdlo, napÅ. jiny-server.com, mÄlo v úmyslu zviditelnit svá data tomuto druhu pÅÃstupu, použÃval se tzv. protokol âJSONPâ (âJSON with paddingâ â âJSON s vycpávkouâ).
Fungovalo to následovnÄ.
ÅeknÄme, že na naÅ¡em sÃdle potÅebujeme zÃskat data z http://jiny-server.com, napÅÃklad informace o poÄasÃ:
-
Nejprve deklarujeme globálnà funkci, která tato data pÅijme, napÅ.
poÄasÃZÃskáno.// 1. Deklarujeme funkci, která bude zpracovávat data o poÄasà function poÄasÃZÃskáno({ teplota, vlhkost }) { alert(`teplota: ${teplota}, vlhkost: ${vlhkost}`); } -
Pak vytvoÅÃme znaÄku
<script>sesrc="http://jiny-server.com/weather.json?callback=poÄasÃZÃskáno". Název našà funkce vložÃme do URL parametrucallback.let skript = document.createElement('script'); skript.src = `http://jiny-server.com/weather.json?callback=poÄasÃZÃskáno`; document.body.append(skript); -
Vzdálený server
jiny-server.comdynamicky vygeneruje skript, který volápoÄasÃZÃskáno(...)s daty, která chce, abychom zÃskali.// OÄekávaná odpovÄÄ ze serveru vypadá takto: poÄasÃZÃskáno({ teplota: 25, vlhkost: 78 }); -
Když se vzdálený skript naÄte a spustÃ, vyvolá se
poÄasÃZÃskáno, a protože je to naÅ¡e funkce, máme data.
To funguje a neporuÅ¡uje to bezpeÄnost, protože obÄ strany souhlasily, že si budou data takto posÃlat. A když obÄ strany souhlasily, nemůže to být hacknutÃ. NÄkteré služby takový pÅÃstup poskytujà dodnes, protože funguje i ve velmi starých prohlÞeÄÃch.
Po nÄjaké dobÄ se v prohlÞeÄovém JavaScriptu objevily sÃÅ¥ové metody.
Požadavky jiného původu byly nejdÅÃve zakázány. Po dlouhých diskusÃch vÅ¡ak byly nakonec povoleny, ale s tÃm, že nové schopnosti vyžadujà výslovné povolenà od serveru, uvedené ve speciálnÃch hlaviÄkách.
BezpeÄné požadavky
Požadavky jiného původu se dÄlà na dva druhy:
- BezpeÄné požadavky.
- VÅ¡echny ostatnÃ.
VytváÅenà bezpeÄných požadavků je jednoduššÃ, zaÄnÄme tedy s nimi.
Požadavek je bezpeÄný, jestliže splÅuje tyto dvÄ podmÃnky:
- BezpeÄná metoda: GET, POST nebo HEAD.
- BezpeÄné hlaviÄky â jsou povoleny jedinÄ tyto vlastnà hlaviÄky:
Accept,Accept-Language,Content-Language,Content-Types hodnotouapplication/x-www-form-urlencoded,multipart/form-datanebotext/plain.
Jakýkoli jiný požadavek se považuje za ânebezpeÄnýâ. NapÅÃklad požadavek s metodou PUT nebo s HTTP hlaviÄkou API-Key tato omezenà nesplÅuje.
Podstatným rozdÃlem je, že bezpeÄný požadavek je možné vytvoÅit pomocà <form> nebo <script>, bez jakýchkoli speciálnÃch metod.
I velmi starý server by tedy mÄl být pÅipraven pÅijmout bezpeÄný požadavek.
Naproti tomu požadavky s nestandardnÃmi hlaviÄkami nebo napÅ. s metodou DELETE nemohou být vytvoÅeny tÃmto způsobem. JavaScript dlouho nedokázal takové požadavky vytváÅet. Staršà server tedy může pÅedpokládat, že takové požadavky pÅicházejà z privilegovaného zdroje, âprotože webová stránka je nenà schopna posÃlatâ.
Když se pokouÅ¡Ãme vytvoÅit nebezpeÄný požadavek, prohlÞeÄ poÅ¡le speciálnà âpÅedbÄžnýâ (âpreflightâ) požadavek, který se zeptá serveru: souhlasÃÅ¡ s pÅijetÃm takového požadavku jiného původu, nebo ne?
A pokud k tomu server výslovnÄ nedá souhlas v hlaviÄkách odpovÄdi, nebezpeÄný požadavek nebude poslán.
Nynà pojÄme do detailů.
CORS pro bezpeÄné požadavky
Pokud požadavek je z jiného původu, prohlÞeÄ do nÄj vždy pÅidá hlaviÄku Origin.
NapÅÃklad pokud posÃláme požadavek na https://kdekoli.com/request z https://javascript.info/page, hlaviÄky budou vypadat takto:
GET /request
Host: kdekoli.com
Origin: https://javascript.info
...
Jak vidÃte, hlaviÄka Origin obsahuje jedinÄ původ (doménu/protokol/port) bez cesty.
Server může Origin prozkoumat, a pokud s pÅijetÃm takového požadavku souhlasÃ, pÅidá do odpovÄdi speciálnà hlaviÄku Access-Control-Allow-Origin. Tato hlaviÄka by mÄla obsahovat povolený původ (v naÅ¡em pÅÃpadÄ https://javascript.info) nebo hvÄzdiÄku *. Pak je odpovÄÄ ÃºspÄÅ¡ná. V opaÄném pÅÃpadÄ nastane chyba.
ProhlÞeÄ zde hraje roli důvÄryhodného prostÅednÃka:
- Zajišťuje, že v požadavku jiného původu je odeslán správný
Origin. - OvÄÅÃ, zda odpovÄÄ obsahuje
Access-Control-Allow-Origins povolenÃm. Pokud ano, povolà JavaScriptu pÅÃstup k odpovÄdi, v opaÄném pÅÃpadÄ vyvolá chybu.
Zde je pÅÃklad odpovÄdi serveru s povolenÃm:
200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info
HlaviÄky odpovÄdi
U požadavku jiného původu může JavaScript standardnÄ pÅistupovat jen k tzv. âbezpeÄnýmâ hlaviÄkám odpovÄdi:
Cache-ControlContent-LanguageContent-LengthContent-TypeExpiresLast-ModifiedPragma
PÅÃstup ke kterékoli jiné hlaviÄce vyvolá chybu.
Aby server povolil JavaScriptu pÅÃstup k jiným hlaviÄkám odpovÄdi, musà poslat hlaviÄku Access-Control-Expose-Headers, která obsahuje seznam názvů nebezpeÄných hlaviÄek, které majà být zpÅÃstupnÄny, oddÄlených Äárkou.
PÅÃklad:
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
Content-Encoding: gzip
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Encoding,API-Key
S takovou hlaviÄkou Access-Control-Expose-Headers má skript dovoleno ÄÃst hlaviÄky odpovÄdi Content-Encoding a API-Key.
âNebezpeÄnéâ požadavky
Můžeme použÃt jakoukoli HTTP metodu: nejenom GET/POST, ale také PATCH, DELETE i jiné.
PÅed nÄjakou dobou si nikdo neumÄl ani pÅedstavit, že by webová stránka mohla vytváÅet takové požadavky. Stále tedy mohou existovat webové služby, které nestandardnà metodu považujà za signál: âTohle nenà prohlÞeÄ.â Mohou to vzÃt v úvahu, když budou ovÄÅovat pÅÃstupová práva.
Abychom tedy pÅedeÅ¡li nedorozumÄnÃ, když jde o ânebezpeÄnýâ požadavek, jaký nemohl být v dÅÃvÄjšà dobÄ vytvoÅen, prohlÞeÄ takový požadavek neposÃlá okamžitÄ. NapÅed poÅ¡le pÅedbÄžný požadavek, tzv. âpreflightâ, kterým požádá o povolenÃ.
PÅedbÄžný požadavek použÃvá metodu OPTIONS, nemá žádné tÄlo a má tÅi hlaviÄky:
- HlaviÄka
Access-Control-Request-Methodobsahuje metodu nebezpeÄného požadavku. - HlaviÄka
Access-Control-Request-Headersposkytuje seznam jeho nebezpeÄných HTTP hlaviÄek, oddÄlených Äárkou. - HlaviÄka
OriginsdÄluje, odkud požadavek pÅiÅ¡el (napÅ.https://javascript.info).
Pokud server souhlasà s obsluhovánÃm takových požadavků, mÄl by poslat odpovÄÄ s prázdným tÄlem, statusem 200 a hlaviÄkami:
Access-Control-Allow-Originmusà být buÄ*, nebo požadovaný původ, napÅ.https://javascript.info, který má být povolen.Access-Control-Allow-Methodsmusà obsahovat povolenou metodu.Access-Control-Allow-Headersmusà obsahovat seznam povolených hlaviÄek.- NavÃc hlaviÄka
Access-Control-Max-Agemůže specifikovat Äas v sekundách, jak dlouho si server bude povolenà pamatovat. ProhlÞeÄ tedy v tomto Äase nebude muset posÃlat pÅedbÄžné požadavky pÅed dalÅ¡Ãmi požadavky, které splÅujà daná povolenÃ.
PodÃvejme se krok za krokem, jak to funguje, na pÅÃkladu požadavku PATCH jiného původu (tato metoda se Äasto použÃvá k aktualizaci dat):
let odpovÄÄ = await fetch('https://site.com/service.json', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'API-Key': 'secret'
}
});
Tento požadavek má tÅi důvody, proÄ je nebezpeÄný (staÄil by jeden):
- Metoda
PATCH. Content-Typenenà jeden z:application/x-www-form-urlencoded,multipart/form-data,text/plain.- âNebezpeÄnáâ hlaviÄka
API-Key.
Krok 1 (pÅedbÄžný požadavek)
PÅed odeslánÃm takového požadavku prohlÞeÄ sám o sobÄ odeÅ¡le pÅedbÄžný požadavek, který vypadá následovnÄ:
OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key
- Metoda:
OPTIONS. - Cesta je pÅesnÄ stejná, jako v hlavnÃm požadavku:
/service.json. - Speciálnà hlaviÄky jiného původu:
Originâ původ zdroje.Access-Control-Request-Methodâ požadovaná metoda.Access-Control-Request-Headersâ seznam ânebezpeÄnýchâ hlaviÄek oddÄlených Äárkou.
Krok 2 (pÅedbÄžná odpovÄÄ)
Server by mÄl odpovÄdÄt statusem 200 a tÄmito hlaviÄkami:
Access-Control-Allow-Origin: https://javascript.infoAccess-Control-Allow-Methods: PATCHAccess-Control-Allow-Headers: Content-Type,API-Key
To umožnà následnou komunikaci, jinak bude vyvolána chyba.
Pokud server v budoucnu oÄekává i jiné metody a hlaviÄky, má smysl je s pÅedstihem povolit a zahrnout do seznamu.
NapÅÃklad tato odpovÄÄ povoluje také metody PUT, DELETE a dalšà hlaviÄky:
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400
Nynà prohlÞeÄ vidÃ, že PATCH je v Access-Control-Allow-Methods a Content-Type,API-Key jsou v seznamu Access-Control-Allow-Headers, takže poÅ¡le hlavnà požadavek.
Pokud je uvedena hlaviÄka Access-Control-Max-Age s Äasem v sekundách, pak se pÅedbÄžná povolenà na zadanou dobu uložà do pamÄti. Uvedená odpovÄÄ bude v pamÄti 86400 sekund (jeden den). Po tuto dobu se pÅed dalÅ¡Ãmi požadavky nebude posÃlat pÅedbÄžný požadavek. Pokud budou splÅovat uvedená povolenÃ, budou odeslány rovnou.
Krok 3 (skuteÄný požadavek)
Když je pÅedbÄžný požadavek úspÄÅ¡ný, prohlÞeÄ vytvoÅà hlavnà požadavek. Je to stejný proces jako u bezpeÄných požadavků.
Hlavnà požadavek obsahuje hlaviÄku Origin (protože je jiného původu):
PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascript.info
Krok 4 (skuteÄná odpovÄÄ)
Server by nemÄl zapomenout pÅidat do hlavnà odpovÄdi Access-Control-Allow-Origin. ÃspÄÅ¡ný pÅedbÄžný požadavek ho z toho nevyvazuje:
Access-Control-Allow-Origin: https://javascript.info
Pak je JavaScript schopen naÄÃst hlavnà odpovÄÄ serveru.
PÅedbÄžné požadavky se odehrávajà âza scénouâ a pro JavaScript jsou neviditelné.
JavaScript dostane jen odpovÄÄ na hlavnà požadavek anebo chybu, pokud server nevrátil povolenÃ.
PÅihlaÅ¡ovacà údaje
Požadavek jiného původu vytvoÅený JavaScriptovým kódem standardnÄ neobsahuje žádné pÅihlaÅ¡ovacà údaje (cookies nebo HTTP autentifikaci).
Pro HTTP požadavky je to neobvyklé. Požadavek na http://site.com je zpravidla doprovázen vÅ¡emi cookies z této domény. Požadavky jiného původu vytvoÅené metodami v JavaScriptu jsou vÅ¡ak výjimkou.
NapÅÃklad fetch('http://jiny-server.com') nepoÅ¡le žádné cookies, dokonce ani ty (!), které patÅà doménÄ jiny-server.com.
ProÄ?
Je to proto, že požadavek s pÅihlaÅ¡ovacÃmi údaji je mnohem silnÄjšà než bez nich. Kdyby byly povoleny, poskytly by JavaScriptu plnou moc jednat jménem uživatele a skrz jeho pÅihlaÅ¡ovacà údaje pÅistupovat k důvÄrným informacÃm.
Co když server opravdu skriptu vÄÅà natolik, že mu to chce umožnit? Pak musà v dalšà hlaviÄce výslovnÄ povolit požadavky s pÅihlaÅ¡ovacÃmi údaji.
Abychom ve fetch poslali pÅihlaÅ¡ovacà údaje, musÃme pÅidat volbu credentials: "include", napÅÃklad:
fetch('http://jiny-server.com', {
credentials: "include"
});
Nynà fetch posÃlá cookies pocházejÃcà z jiny-server.com spolu s požadavkem na toto sÃdlo.
Jestliže server souhlasà s pÅijetÃm požadavku s pÅihlaÅ¡ovacÃmi údaji, mÄl by do odpovÄdi kromÄ hlaviÄky Access-Control-Allow-Origin pÅidat i hlaviÄku Access-Control-Allow-Credentials: true.
PÅÃklad:
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
ProsÃme vÅ¡imnÄte si, že Access-Control-Allow-Origin nesmà u požadavků s pÅihlaÅ¡ovacÃmi údaji obsahovat hvÄzdiÄku *. Jak zde vidÃme, musà tentokrát poskytnout pÅesný původ. To je dalšà bezpeÄnostnà opatÅenÃ, které zajistÃ, že server opravdu vÃ, komu důvÄÅuje natolik, aby mu povolil takové požadavky.
ShrnutÃ
Z pohledu prohlÞeÄe existujà dva druhy požadavků jiného původu: âbezpeÄnéâ a vÅ¡echny ostatnÃ.
âBezpeÄnéâ požadavky musejà splÅovat následujÃcà podmÃnky:
- Metoda: GET, POST nebo HEAD.
- HlaviÄky â můžeme nastavit jedinÄ tyto:
AcceptAccept-LanguageContent-LanguageContent-Typena hodnotuapplication/x-www-form-urlencoded,multipart/form-datanebotext/plain.
Podstatným rozdÃlem je, že bezpeÄné požadavky byly proveditelné již pÅed dlouhou dobou pomocà znaÄek <form> nebo <script>, zatÃmco nebezpeÄné nemohly prohlÞeÄe dlouhou dobu vůbec vytváÅet.
Praktický rozdÃl je tedy v tom, že bezpeÄné požadavky se posÃlajà rovnou a s hlaviÄkou Origin, zatÃmco pro ostatnà prohlÞeÄ vytvoÅà pÅedbÄžný požadavek, tzv. âpreflightâ, kterým žádá o povolenÃ.
Pro bezpeÄné požadavky:
- â ProhlÞeÄ posÃlá hlaviÄku
Origins původem. - â Pro požadavky bez pÅihlaÅ¡ovacÃch údajů (ty se standardnÄ neposÃlajÃ) by server mÄl nastavit:
Access-Control-Allow-Originna*nebo na stejnou hodnotu jakoOrigin
- â Pro požadavky s pÅihlaÅ¡ovacÃmi údaji by server mÄl nastavit:
Access-Control-Allow-Originna stejnou hodnotu jakoOriginAccess-Control-Allow-Credentialsnatrue
NavÃc aby server umožnil JavaScriptu pÅÃstup k jiným hlaviÄkám odpovÄdi než Cache-Control, Content-Language, Content-Type, Expires, Last-Modified nebo Pragma, mÄl by pÅÃsluÅ¡né hlaviÄky vyjmenovat v seznamu v hlaviÄceAccess-Control-Expose-Headers.
Pro nebezpeÄné požadavky je pÅed skuteÄným požadavkem vydán pÅedbÄžný požadavek, tzv. âpreflightâ:
- â ProhlÞeÄ poÅ¡le na stejné URL požadavek
OPTIONSs tÄmito hlaviÄkami:Access-Control-Request-Methodobsahuje požadovanou metodu.Access-Control-Request-Headersobsahuje seznam nebezpeÄných hlaviÄek požadavku.
- â Server by mÄl odpovÄdÄt statusem 200 a tÄmito hlaviÄkami:
Access-Control-Allow-Methodsse seznamem povolených metod,Access-Control-Allow-Headersse seznamem povolených hlaviÄek,Access-Control-Max-Ages Äasem v sekundách, jak dlouho si bude povolenà pamatovat.
- Pak je odeslán skuteÄný požadavek a aplikuje se pÅedchozà âbezpeÄnéâ schéma.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)