Pokud odešleme fetch
požadavek na jiný web, pravděpodobně selže.
Zkusme například načíst http://example.com
:
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // Failed to fetch
}
Načítání se nezdařilo, jak se očekávalo.
Základním konceptem je zde původ – triplet doména/port/protokol.
Cross-origin požadavky – ty odeslané do jiné domény (dokonce i subdomény) nebo protokolu či portu – vyžadují speciální hlavičky ze vzdálené strany.
Tato zásada se nazývá „CORS“:Cross-Origin Resource Sharing.
Proč je potřeba CORS? Stručná historie
CORS existuje, aby chránil internet před zlými hackery.
Vážně. Udělejme velmi krátkou historickou odbočku.
Po mnoho let neměl skript z jednoho webu přístup k obsahu jiného webu.
Toto jednoduché, ale účinné pravidlo bylo základem internetové bezpečnosti. Např. zlý skript z webu hacker.com
nemohl získat přístup k poštovní schránce uživatele na webu gmail.com
. Lidé se cítili bezpečně.
JavaScript také v té době neměl žádné speciální metody pro provádění síťových požadavků. Byl to hračkářský jazyk k ozdobení webové stránky.
Weboví vývojáři však požadovali více výkonu. Byla vynalezena řada triků, jak obejít omezení a odeslat požadavky na jiné webové stránky.
Používání formulářů
Jedním ze způsobů, jak komunikovat s jiným serverem, bylo odeslat <form>
tam. Lidé jej odeslali do <iframe>
, stačí zůstat na aktuální stránce, například takto:
<!-- form target -->
<iframe name="iframe"></iframe>
<!-- a form could be dynamically generated and submited by JavaScript -->
<form target="iframe" method="POST" action="http://another.com/…">
...
</form>
Bylo tedy možné provést požadavek GET/POST na jiný web, a to i bez síťových metod, protože formuláře mohou odesílat data kamkoli. Ale protože je zakázán přístup k obsahu <iframe>
z jiného webu, nebylo možné přečíst odpověď.
Abych byl přesný, byly na to vlastně triky, vyžadovaly speciální skripty jak na iframe, tak na stránce. Takže komunikace s iframe byla technicky možná. Právě teď nemá smysl zabíhat do podrobností, nechte tyto dinosaury odpočívat v pokoji.
Používání skriptů
Dalším trikem bylo použití script
štítek. Skript může mít libovolný src
, s libovolnou doménou, například <script src="http://another.com/…">
. Skript je možné spustit z libovolného webu.
Pokud web, např. another.com
zamýšlel vystavit data pro tento druh přístupu, pak byl použit takzvaný protokol „JSONP (JSON with padding)“.
Zde je návod, jak to fungovalo.
Řekněme, že na našem webu potřebujeme získat data z http://another.com
, jako je počasí:
-
Nejprve předem deklarujeme globální funkci pro akceptování dat, např.
gotWeather
.// 1. Declare the function to process the weather data function gotWeather({ temperature, humidity }) { alert(`temperature: ${temperature}, humidity: ${humidity}`); }
-
Poté vytvoříme
<script>
tag ssrc="http://another.com/weather.json?callback=gotWeather"
, s použitím názvu naší funkce jakocallback
URL-parametr.let script = document.createElement('script'); script.src = `http://another.com/weather.json?callback=gotWeather`; document.body.append(script);
-
Vzdálený server
another.com
dynamicky generuje skript, který volágotWeather(...)
s údaji, které chceme, abychom obdrželi.// The expected answer from the server looks like this: gotWeather({ temperature: 25, humidity: 78 });
-
Když se vzdálený skript načte a spustí,
gotWeather
běží, a protože je to naše funkce, máme data.
To funguje a nenarušuje bezpečnost, protože obě strany souhlasily s předáváním dat tímto způsobem. A když se obě strany dohodnou, rozhodně to není hack. Stále existují služby, které takový přístup poskytují, protože funguje i pro velmi staré prohlížeče.
Po chvíli se v JavaScriptu prohlížeče objevily síťové metody.
Zpočátku byly žádosti o cross-origin zakázány. Ale v důsledku dlouhých diskusí byly povoleny požadavky napříč původem, ale s jakýmikoli novými možnostmi vyžadujícími explicitní povolení ze strany serveru, vyjádřené ve speciálních hlavičkách.
Bezpečné požadavky
Existují dva typy požadavků napříč původem:
- Bezpečné požadavky.
- Všechny ostatní.
Bezpečné požadavky jsou jednodušší na vytváření, začněme jimi.
Požadavek je bezpečný, pokud splňuje dvě podmínky:
- Bezpečná metoda:GET, POST nebo HEAD
- Bezpečná záhlaví – jediná povolená vlastní záhlaví jsou:
Accept
,Accept-Language
,Content-Language
,Content-Type
s hodnotouapplication/x-www-form-urlencoded
,multipart/form-data
nebotext/plain
.
Jakýkoli jiný požadavek je považován za „nebezpečný“. Například požadavek s PUT
nebo pomocí API-Key
HTTP hlavička nesplňuje omezení.
Zásadní rozdíl je v tom, že bezpečný požadavek lze provést pomocí <form>
nebo <script>
, bez jakýchkoli speciálních metod.
Takže i velmi starý server by měl být připraven přijmout bezpečný požadavek.
Naproti tomu požadavky s nestandardními hlavičkami nebo např. metoda DELETE
nelze tímto způsobem vytvořit. Po dlouhou dobu nebyl JavaScript schopen takové požadavky provádět. Takže starý server může předpokládat, že takové požadavky pocházejí z privilegovaného zdroje, „protože je webová stránka nemůže odeslat“.
Když se pokusíme vytvořit nebezpečný požadavek, prohlížeč odešle speciální požadavek „před výstupem“, který se serveru zeptá – souhlasí s přijetím takových požadavků z různých zdrojů, nebo ne?
A pokud server výslovně nepotvrdí, že s hlavičkami, není nebezpečný požadavek odeslán.
Nyní půjdeme do podrobností.
CORS pro bezpečné požadavky
Pokud je požadavek cross-origin, prohlížeč vždy přidá Origin
záhlaví.
Pokud například požadujeme https://anywhere.com/request
od https://javascript.info/page
, budou záhlaví vypadat takto:
GET /request
Host: anywhere.com
Origin: https://javascript.info
...
Jak můžete vidět, Origin
hlavička obsahuje přesně původ (doménu/protokol/port), bez cesty.
Server může zkontrolovat Origin
a pokud souhlasí s přijetím takového požadavku, přidá speciální hlavičku Access-Control-Allow-Origin
na odpověď. Tato hlavička by měla obsahovat povolený původ (v našem případě https://javascript.info
), nebo hvězdička *
. Pak je odpověď úspěšná, jinak je to chyba.
Prohlížeč zde hraje roli důvěryhodného prostředníka:
- Zajišťuje správnou hodnotu
Origin
je odeslána s požadavkem na křížový původ. - Zkontroluje povolení
Access-Control-Allow-Origin
v odpovědi, pokud existuje, má JavaScript povolen přístup k odpovědi, jinak selže s chybou.
Zde je příklad permisivní odpovědi serveru:
200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info
Záhlaví odpovědí
U požadavku mezi původem může JavaScript ve výchozím nastavení přistupovat pouze k takzvaným „bezpečným“ hlavičkám odpovědí:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
Přístup k jakékoli jiné hlavičce odpovědi způsobí chybu.
Poznámka:
Neexistuje žádné Content-Length
záhlaví v seznamu!
Tato hlavička obsahuje celou délku odpovědi. Pokud tedy něco stahujeme a chtěli bychom sledovat procento pokroku, pak je pro přístup k této hlavičce vyžadováno další oprávnění (viz níže).
Chcete-li udělit přístup JavaScript jakékoli jiné hlavičce odpovědi, musí server odeslat Access-Control-Expose-Headers
záhlaví. Obsahuje čárkami oddělený seznam nebezpečných názvů hlaviček, které by měly být zpřístupněny.
Například:
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Length,API-Key
S takovým Access-Control-Expose-Headers
záhlaví, skript smí číst Content-Length
a API-Key
záhlaví odpovědi.
„Nebezpečné“ požadavky
Můžeme použít jakoukoli metodu HTTP:nejen GET/POST
, ale také PATCH
, DELETE
a další.
Před časem si nikdo nedokázal ani představit, že by webová stránka mohla klást takové požadavky. Stále tedy mohou existovat webové služby, které považují nestandardní metodu za signál:„To není prohlížeč“. Mohou to vzít v úvahu při kontrole přístupových práv.
Aby nedocházelo k nedorozuměním, jakýkoli „nebezpečný“ požadavek – který za starých časů nebylo možné provést, prohlížeč takové požadavky nedává hned. Nejprve odešle předběžnou, takzvanou „předletovou“ žádost o povolení.
Předtiskový požadavek používá metodu OPTIONS
, žádné tělo a tři záhlaví:
Access-Control-Request-Method
hlavička má metodu nebezpečného požadavku.Access-Control-Request-Headers
hlavička poskytuje seznam nebezpečných HTTP hlaviček oddělených čárkami.Origin
hlavička říká, odkud požadavek přišel. (napříkladhttps://javascript.info
)
Pokud server souhlasí se zpracováním požadavků, měl by odpovědět prázdným tělem, stavem 200 a záhlavími:
Access-Control-Allow-Origin
musí být buď*
nebo žádající původ, napříkladhttps://javascript.info
, abyste to povolili.Access-Control-Allow-Methods
musí mít povolenou metodu.Access-Control-Allow-Headers
musí mít seznam povolených záhlaví.- Navíc záhlaví
Access-Control-Max-Age
může zadat počet sekund pro uložení oprávnění do mezipaměti. Prohlížeč tedy nebude muset posílat předběžnou kontrolu pro následné požadavky, které splňují daná oprávnění.
Podívejme se, jak to funguje, krok za krokem na příkladu cross-origin PATCH
požadavek (tato metoda se často používá k aktualizaci dat):
let response = await fetch('https://site.com/service.json', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'API-Key': 'secret'
}
});
Existují tři důvody, proč není požadavek bezpečný (stačí jeden):
- Metoda
PATCH
Content-Type
není jedno z:application/x-www-form-urlencoded
,multipart/form-data
,text/plain
.- „Nebezpečné“
API-Key
záhlaví.
Krok 1 (požadavek před výstupem)
Před odesláním takového požadavku prohlížeč sám odešle požadavek před výstupem, který vypadá takto:
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 – přesně stejná jako hlavní požadavek:
/service.json
. - Speciální záhlaví napříč původy:
Origin
– zdroj původu.Access-Control-Request-Method
– požadovaná metoda.Access-Control-Request-Headers
– seznam „nebezpečných“ záhlaví oddělených čárkami.
Krok 2 (odpověď před výstupem)
Server by měl odpovědět stavem 200 a hlavičkami:
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PATCH
Access-Control-Allow-Headers: Content-Type,API-Key
.
To umožňuje budoucí komunikaci, jinak dojde k chybě.
Pokud server očekává v budoucnu další metody a hlavičky, má smysl je povolit předem přidáním do seznamu.
Tato odpověď například také umožňuje PUT
, DELETE
a další záhlaví:
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 odešle hlavní požadavek.
Pokud je tam záhlaví Access-Control-Max-Age
s počtem sekund, pak jsou oprávnění pro kontrolu před výstupem uložena do mezipaměti pro daný čas. Výše uvedená odpověď bude uložena v mezipaměti po dobu 86 400 sekund (jeden den). V tomto časovém rámci následné požadavky nezpůsobí předletovou kontrolu. Za předpokladu, že vyhovují povolenkám uloženým v mezipaměti, budou odeslány přímo.
Krok 3 (skutečný požadavek)
Když je kontrola před výstupem úspěšná, prohlížeč nyní zadá hlavní požadavek. Postup je zde stejný jako u bezpečných požadavků.
Hlavní požadavek má Origin
záhlaví (protože jde o křížový původ):
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 Access-Control-Allow-Origin
na hlavní odpověď. Úspěšná příprava před výstupem z toho neuleví:
Access-Control-Allow-Origin: https://javascript.info
Potom je JavaScript schopen číst odpověď hlavního serveru.
Poznámka:Požadavek na kontrolu před výstupem probíhá „za scénou“, je neviditelný pro JavaScript.
JavaScript obdrží odpověď na hlavní požadavek nebo chybu pouze v případě, že neexistuje žádné oprávnění serveru.
Přihlašovací údaje
Požadavek napříč původem iniciovaný kódem JavaScript ve výchozím nastavení nepřináší žádné přihlašovací údaje (soubory cookie nebo ověřování HTTP).
To je u požadavků HTTP neobvyklé. Obvykle požadavek na http://site.com
je doprovázeno všemi soubory cookie z této domény. Na druhou stranu požadavky cross-origin provedené metodami JavaScriptu jsou výjimkou.
Například fetch('http://another.com')
neposílá žádné cookies, ani ty (!), které patří another.com
domény.
Proč?
Je to proto, že žádost s přihlašovacími údaji je mnohem výkonnější než bez nich. Pokud je to povoleno, uděluje JavaScriptu plnou pravomoc jednat jménem uživatele a přistupovat k citlivým informacím pomocí jeho přihlašovacích údajů.
Opravdu server tolik důvěřuje skriptu? Poté musí explicitně povolit požadavky s přihlašovacími údaji s další hlavičkou.
Chcete-li odeslat přihlašovací údaje v fetch
, musíme přidat volbu credentials: "include"
, takto:
fetch('http://another.com', {
credentials: "include"
});
Nyní fetch
odesílá soubory cookie pocházející z another.com
s požadavkem na daný web.
Pokud server souhlasí s přijetím požadavku s přihlašovacími údaji , měl by přidat záhlaví Access-Control-Allow-Credentials: true
na odpověď, kromě Access-Control-Allow-Origin
.
Například:
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
Poznámka:Access-Control-Allow-Origin
je zakázáno používat hvězdičku *
pro žádosti s přihlašovacími údaji. Jak je uvedeno výše, musí tam uvádět přesný původ. To je další bezpečnostní opatření, které zajistí, že server skutečně ví, komu důvěřuje, že takové požadavky podává.
Shrnutí
Z pohledu prohlížeče existují dva druhy požadavků z různých zdrojů:„bezpečné“ a všechny ostatní.
„Bezpečné“ požadavky musí splňovat následující podmínky:
- Metoda:GET, POST nebo HEAD.
- Záhlaví – můžeme nastavit pouze:
Accept
Accept-Language
Content-Language
Content-Type
na hodnotuapplication/x-www-form-urlencoded
,multipart/form-data
nebotext/plain
.
Zásadní rozdíl je v tom, že bezpečné požadavky byly proveditelné od starověku pomocí <form>
nebo <script>
tagy, zatímco nebezpečné byly pro prohlížeče po dlouhou dobu nemožné.
Praktický rozdíl je tedy v tom, že bezpečné požadavky jsou odesílány okamžitě s Origin
záhlaví, zatímco u ostatních prohlížeč předběžně požádá o „preflight“ žádost o povolení.
Pro bezpečné požadavky:
- → Prohlížeč odešle
Origin
záhlaví s původem. - ← Pro požadavky bez přihlašovacích údajů (ve výchozím nastavení se neodesílají) by měl server nastavit:
Access-Control-Allow-Origin
na*
nebo stejnou hodnotu jakoOrigin
- ← Pro požadavky s přihlašovacími údaji by měl server nastavit:
Access-Control-Allow-Origin
na stejnou hodnotu jakoOrigin
Access-Control-Allow-Credentials
natrue
Navíc udělit JavaScript přístup ke všem hlavičkám odpovědí kromě Cache-Control
, Content-Language
, Content-Type
, Expires
, Last-Modified
nebo Pragma
, server by měl ty povolené vypsat v Access-Control-Expose-Headers
záhlaví.
V případě nebezpečných požadavků je před požadovaným vydán předběžný požadavek „před výstupem“:
- → Prohlížeč odešle
OPTIONS
požadavek na stejnou adresu URL se záhlavím:Access-Control-Request-Method
má požadovanou metodu.Access-Control-Request-Headers
uvádí nebezpečná požadovaná záhlaví.
- ← Server by měl odpovědět stavem 200 a hlavičkami:
Access-Control-Allow-Methods
se seznamem povolených metod,Access-Control-Allow-Headers
se seznamem povolených záhlaví,Access-Control-Max-Age
s počtem sekund na uložení oprávnění do mezipaměti.
- Potom se odešle skutečný požadavek a použije se předchozí „bezpečné“ schéma.