Čtvrt století s CSRF. Teď ho konečně řeší prohlížeč
Cross-Site Request Forgery je s námi minimálně od přelomu tisíciletí. Princip je primitivní: cizí web pošle z prohlížeče nic netušící oběti požadavek na váš server. Prohlížeč k němu ochotně přibalí cookies, takže request vypadá jako legitimní akce přihlášeného uživatele: změna hesla, odeslání formuláře, smazání účtu.
A celé čtvrtstoletí se to řešilo chabě, protézami na straně aplikací.
Éra tokenů a triků
Klasická obrana: vygenerovat náhodný token, schovat ho do formuláře,
uložit do session a při odeslání porovnat. Funguje to, ale je to upatlané:
token musí být v každém formuláři, musí někde žít, expiruje, zlobí
s cache i s AJAXem. V Nette to znamenalo volat
$form->addProtection() a hlavně na to nikde nezapomenout.
Jediný formulář bez tokenu = díra.
První velká úleva přišla se SameSite
cookies: cookie jde označit atributem, aby ji prohlížeč k požadavkům
z cizích webů neposílal. Nette na tom postavilo vlastní ochranu: posílalo
speciální cookie _nss (dříve nette-samesite) se
SameSite=Strict a metoda isSameSite() podle její
přítomnosti poznala, že request nepřišel z cizího webu. Šikovný trik,
ale s vrozenými limity. Cookie umí rozlišit jen site, ne
origin: požadavek z libovolné subdomény vypadá stejně jako přímo
z webu, takže subdoména s uživatelským obsahem projde kontrolou taky.
A pořád je to stavová informace, posíláme cookie navíc v každém
requestu.
Sec-Fetch hlavičky: Konečně systémové řešení!
A pak prohlížeče přidaly Fetch Metadata: hlavičky
Sec-Fetch-*, kterými v každému requestu samy říkají, odkud a
proč přišel:
Sec-Fetch-Site: cross-site (request inicioval cizí web)
Sec-Fetch-Dest: document (cílem je navigace na stránku)
Sec-Fetch-User: ?1 (akci vyvolal sám uživatel)
Žádný token, žádná cookie, žádný stav na serveru. Hlavičky
s prefixem Sec- nedovolí prohlížeč nastavit ani JavaScriptu,
ani rozšířením. Mimo prohlížeč si je samozřejmě dopíše kdokoli,
jenže cURL útočníka nemá cookies oběti, takže pro CSRF je to
bezpředmětné. Podstatné je, že prohlížeč oběti řekne vždycky
pravdu.
Nette s nasazením schválně vyčkávalo: hlavičky sice existují od roku 2019, ale jako poslední je doplnilo Safari až ve verzi 16.4 (březen 2023). Konečně je adopce dostatečně široká a tak přichází nové API.
isFrom(): řekněte si, odkud smí request přijít
use Nette\Http\FetchSite;
use Nette\Http\FetchDest;
// přišel request z mého originu?
$request->isFrom(FetchSite::SameOrigin);
// je to navigace na stránku, kterou na mém originu vyvolal sám uživatel?
$request->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true);
První parametr odpovídá hlavičce Sec-Fetch-Site (enum
FetchSite), volitelně lze zpřesnit cíl requestu (enum
FetchDest: dokument, obrázek, skript, fetch…) a ověřit si, že
akci spustil sám uživatel.
Všimněte si, že na rozdíl od cookie už jde rozlišit
SameOrigin od SameSite: same-site povoluje
i subdomény, same-origin vyžaduje přesnou shodu schématu, domény i portu.
Pro citlivé akce volte FetchSite::SameOrigin.
Původní isSameSite() zůstává funkční, jen je deprecated a
interně deleguje právě na isFrom().
Fallback pro staré iOS
Jedna zadní vrátka zatím raději zůstala: prohlížeče bez podpory Sec-Fetch, což dnes znamená hlavně Safari starší než 16.4. A protože na iOS je každý prohlížeč pouhou nadstavbou nad systémovým WebKitem, týká se to starších iPhonů a iPadů bez ohledu na to, jaký prohlížeč na nich běží.
V tomto případě nastupuje stará dobrá _nss cookie. Nette
ji nově posílá pouze prohlížečům, které hlavičku
Sec-Fetch-Site neposlaly, všude jinde už ochrana běží úplně
bez cookies.
Nette to za vás dělá automaticky
Možná to ani nevíte: formuláře se v Nette brání samy už dávno. Od
nette/forms 3.1 se POST přicházející z cizího webu vůbec nepřijme,
dokud to explicitně nepovolíte přes
$form->allowCrossOrigin(). A signály v nette/application jsou
automaticky chráněné, jako by měly atribut
#[Requires(sameOrigin: true)]. Obojí dosud interně stálo na
isSameSite(), tedy na _nss cookie.
Co se teď změní:
- Hned s nette/http 3.4 přechází
isSameSite()interně na Sec-Fetch hlavičky, takže existující automatická ochrana formulářů i signálů se zpřesní bez jediné změny ve vašem kódu. Jedna viditelná změna chování: přímá navigace (záložka, ručně zadaná adresa, odkaz z e-mailu) se za same-site už nepovažuje; dříve procházela díky strict cookie. Pokud nějaký váš signál spoléhá na akční odkazy v e-mailech, označte ho#[Requires(sameOrigin: false)]. - S nette/forms 3.3 a nette/application 3.3 přejde obojí z
isSameSite()naisFrom(FetchSite::SameOrigin)a ochrana bude ještě o fous přísnější: requesty ze subdomén už neprojdou. AtributsameOrigintak bude poprvé kontrolovat skutečně origin, ne jen site.
Konec addProtection()
A tím se dostáváme k pointě: $form->addProtection()
vlastně nepotřebujete už dnes, automatická ochrana kryje totéž. Poslední
rozdíl, tedy ochranu před requesty ze subdomén, smaže přechod na
same-origin ve forms 3.3. Tokeny v session, skrytá pole a přemýšlení,
jestli jsme někde nezapomněli, můžou do důchodu.
Prakticky to znamená: povyšte nette/http na 3.4, u nových formulářů
addProtection() vynechte a ze stávajících ho můžete
začít mazat.
Trvalo to jen čtvrt století.
Nové API přichází s verzí nette/http 3.4.0, forms 3.3 a application 3.3 budou následovat. Stejná verze přidává i ochranu proti SSRF útokům.
Další čtení
- Nette Http 3.4.0 přináší ochranu proti SSRF útokům
- Nette Http 3.2: změna přístupu ke credentials
- Nette Tester: HTTP testování ještě nikdy nebylo tak jednoduché
- Kvíz: ubráníte se před zranitelností XSS?
- Latte 3.1: Když šablonovací systém skutečně rozumí HTML
- Pět novinek v Latte 3.1, které vám zpříjemní život
Chcete-li odeslat komentář, přihlaste se