Portál AbcLinuxu, 1. května 2025 11:07
Rád bych se podělil o třídu, která má bránit proti CSRF útokům.
Jako první bych začal, co to CSRF (Cross-site request forgery) útok vlastně je. Jde o typ útoku, který umožňuje útočníkovi (po spuštění jeho kódu na jeho webové stránce) provést operace na jiné důvěryhodné stránce, kde je uživatel aktuálně přihlášen. Pro příklad: Jste přihlášeni ve webmailu, jenom navštívíte útočníkovu stránku a on vám změní nastavení přeposílání emailů tak, že veškeré vaše nové emaily se přesměrují i do jeho schránky. (více informací o útoku třeba zde či zde)
Vzhledem k tomu, že útočník nepotřebuje najít chybu v kódu stávající stránky k úspěšnému provedení útoku a stačí mu pouze návštěva uživatele na cizí stránce, tak si myslím, že by tento typ útoku neměl být brán na lehkou váhu. Navíc obrana proti němu je dle mého názoru relativně snadná (a snad i s touto třídou snadnější ). Nejjednodušší princip ochrany (a také nejpoužívanější) je používání autorizačního tokenu, který se vkládá jako parametr (GET či POST) nového dotazu do stránky, následně se odešle na server a tam se zkontroluje oproti uložené hodnotě. Používat by se měl při každém dotazu, nebo alespoň u těch, které provádějí na serveru nějakou akci.
Alfou a omegou fungování této ochrany je nemožnost získání tokenu útočníkem. Vzhledem k tomu, že útočník nemá (až na jeden způsob, a to vložením externího javascriptu (JS) do stránky) možnost získat v prohlížeči uživatele obsah chráněné stránky (a tím i token), tak je potřeba vyvarovat se vkládání tokenu v čitelné formě v JS formátu (ať už naprostým nevkládáním tohoto tokenu do JS nebo používáním AJAXu pro získání tokenu či zapouzdřením tokenu v JS tak, aby z venku nešel získat (riskantní způsob, ale někdy to jinak nejde)). Stejně tak je dobré, aby token nebyl funkční po moc dlouhou dobu a aby byl pokaždé unikátní.
Moje třída používá přesně tuto ochranu (tedy používání autorizačního tokenu). Jedna z důležitých vlastností této třídy (a také způsob návrhu) je ta, že na serveru se autorizační token neukládá (na rozdíl třeba od řešení Jakuba Vrány). To sice teoreticky dává možnost útočníkovi odhadnout token, případně podvrhnout klíč i kontrolní cookie, ale jak brzy vysvětlím, proti tomu je třída (dostatečně) chráněna.
Pojďme se nyní dostat k tomu, jak moje třída funguje. Když necháte vygenerovat token, tak se do cookie uloží kontrolní hash a při kontrole se tyto dva údaje proti sobě zkontrolují. Aby to ale nebylo tak jednoduché, tak v tokenu je zakomponováno, kdy byl vytvořen, a v kontrolním hashi je mimo tokenu zakomponován jak čas vytvoření, tak IP adresa uživatele a jeho User Agent - to vše pro to, aby se omezila možnost použít token vícekrát a aby se nedal jinak než z IP adresy uživatele zjistit jiný platný token pro případné podvržení (třeba dotazem z útočníkova serveru). Třída má zároveň možnost povolit více kontrol naráz - buď je nastaveno používání jenom jedné hodnoty v cookie (tedy pokud uživatel má otevřených více stránek naráz, tak bude fungovat jen ta poslední načtená) nebo se v cookie bude ukládat více hodnot a fungovat budou všechny otevřené stránky.
Nyní vás možná napadlo - toto je veřejně dostupná třída a útočník má možnost získat jak IP adresu, tak User Agenta uživatele, tak proč by nemohl vytvořit vlastní platnou dvojici a podvrhnout ji? I na toto jsem při návrhu myslel. Předtím, než začnete třídu používat, si vygenerujete pomocí metody csrfp::random_settings vlastní náhodné nastavení generování tokenu. Možností generování je při standardním nastavení (délka klíče 40 znaků, znaky v klíči 0-9a-zA-Z a 50 znaků salt pro hash) přes 2*10^192 a s postupem času se toto číslo zvětšuje (díky úpravě času v hashi).
//Vytvoření nové instance $csrfp = new csrfp($settings); /* Možnosti nastavení (settings je pole a je to nepovinný parametr konstruktoru; v následujícím popisu je v závorkách uvedena obecná hodnota): length => Délka tokenu (standardně 40 znaků) validity => Délka platnosti v minutách (standardně 20; třída bude funkční až do platnosti 1 dne) cookie => Jméno cookie (csrfp) get => Jméno parametru, když je token v $_GET (csrfp) post => Jméno parametru, když je token v $_POST (csrfp) cookie_properties => Parametry pro funkci setcookie (od path včetně; standardně prázdné a jedná se o pole) multiple => Pokud je true, tak se zapne podpora pro více kontrol naráz (standardně zapnuté). */ //Pro získání nového tokenu je potřeba inicializace; vzhledem k tomu, že se vytváří cookie, tak nesmí být do té doby poslán jakýkoliv obsah $token = $csrfp->start(); //nebo $token = $csrfp->token; //Token je možné po inicializaci získat buď jako $csrfp->token, nebo $csrfp->key nebo jenom pomocí (string) $csrfp //Pro vytištěni odkazu s tokenem echo 'text'; //Kontrola tokenu - vrací true nebo vyhazuje Exception $csrfp->check(); //Smazání cookie unset($csrfp->token); //smaže poslední úspěšně zkontrolovaný token (při multiple; jinak funguje vždy) //nebo $csrfp->disable($token); //parametr nutný jen při zapnutém multiple
Doplnění: Pro správné fungování je potřeba používat token jednorázově, proto se po zkontrolování token smaže z cookie.
Doufám, že vám tato třída pomůže k jednoduché implementaci ochrany proti CSRF. Třída je dostupná pod licencemi MIT a GPLv3.0 a testována v PHP 5.2.10, takže by neměl být problém ji využít téměř kdekoliv (pokud se mýlím, opravte mě v komentářích). Stejně tak pokud byste měli pocit, že třída není bezpečná nebo byste měli jakýkoliv jiný dotaz, napište komentář . Děkuji za čtení.
Update 28.1.2010 11:40: Upravil jsem třídu tak, aby se vždy používalo právě jedno cookie, čímž je bez přístupu útočníka na chráněný server nemožné podvrhnout cookie (pokud je platnost autorizace uživatele na serveru stejná nebo kratší než platnost cookie a cookie je přítomno, tedy ochrana je zapnuta na všech stránkách). Je to díky tomu, že na přepsání cookie je potřeba, aby cookie přišlo ze stejné domény.
Třída (včetně PHPDoc dokumentace) je ke stažení zde.
Tiskni
Sdílej:
a ty nam nereknes, proc je spatne?
...aby se nedal jinak než z IP adresy uživatele zjistit jiný platný token pro případné podvržení (třeba dotazem z útočníkova serveru)
je vložen do tokenu a nelze z něj bez znalosti přesného obsahu třídyTo jste taky nějak zapoměl zmínit. Ono by Vám vůbec neuškodilo kdybyste ten algoritmus trochu formálně popsal. V tom případě nevím na co tam máte to IP a user agent? Celý protokol můžete udělat takto: C->S: GetForm S->C: Form,TimeStamp,Nonce,Cookie=hash(TimeStamp,Nonce) C->S: PostForm,TimeStamp,Nonce,Cookie a dále postupovat stejně. Pak se to více méně podobá klasické ochraně akorát místo uložení na serveru máte "uloženo" v prohlížeči, přičemž spoléháte na tu hash funkci a na ochranu cookies. Jak tak na to koukám tak tam ten timestamp ani hash nemusí být. Co třeba: C->S: GetForm S->C: Form,Nonce,Cookie=Nonce C->S: PostForm,Nonce,Cookie
Timestamp a hash tam být musí. Timestamp je pro zajištění expirace a v cookies je hash, aby se při pouhém přečtení cookies nedal získat tokenPřečtení cookies? To už tady nějak bylo řečeno, že nejde a kdyby šlo tak je to stejně celé k ničemu? A expirace snad ani není v tomto kontextu bezpečnostní prvek. Proti replay nepomáhá, to už bylo taky řečeno.
IP proto, aby nebylo tak snadné podvržení (již to bylo v diskuzi rozebíráno).To jsme právě rozebrali a ta nonce to spolehlivě zajistí i bez IP.
S->C: Form,Token,Cookie=hash(Token,TimeStamp vytvoření,IP,UA,salt)Tohle asi není úplně ono? Co tam dělá ten salt a jak se ho dozví validátor?
kde timestamp se získává z tokenu (a bez konfigurace třídy, která je unikátní pro každou třídu, nelze zjistit; a možností je velmi mnoho, jak jsem zmínil v zápisku)V čem to je lepší než přišpendlení nonce?
Navíc, pokud se nemýlím, tak vámi navržený útok nemůže fungovat - protože projde pouze kombinace (token, cookie) či (token', cookie'), nikoliv (token', cookie).Naopak, můžete poslat jen (cokoliv, cookie), protože cookie tam dodá prohlížeč. To byla Vaše základní premisa a na tom to celé stojí (a padá:)
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.