Cross-Site Request Forgery

Školení, která pořádám

O útoku CSRF se neví tolik, jako např. o XSS nebo SQL Injection. Pěkně o něm píše třeba Chris Shiflett nebo Llaik (byť s ním v části výkladu nesouhlasím).

Útok může být snadno veden proti aplikacím, které pro uchování informace o přihlášenosti uživatele používají cookie (tedy typicky i session proměnné PHP) nebo hlavičku Authorization. Podstata útoku spočívá v tom, že uživatele přimějeme navštívit stránku napadané aplikace, která provádí nějakou akci, aniž by o tom uživatel věděl. Útok tím pádem může být snadno veden proti aplikacím, do kterých se útočník může sám přihlásit a tím zjistit jejich strukturu nebo které mají přístupný zdrojový kód. Ale ani ostatní aplikace nejsou chráněny, URL např. ve tvaru /admin/clanky.php?action=delete&id=7 se dá prostě uhodnout.

Transparentní navštívení aplikace se dá jednoduše zařídit např. pomocí značky img, které do parametru src dáme URL stránky, kterou chceme, aby uživatel navštívil. Tuto značku dáme na jakoukoliv stránku a počkáme, až ji navštíví nějaký přihlášený uživatel. Prohlížeč potom spolu s požadavkem na dané URL pošle i autorizační cookie. Server mu sice místo obrázku vrátí prosté HTML, ale na to prohlížeč zareaguje pouze zobrazením ikony pro nefunkční obrázek (a pokud má obrázek nastavené dostatečně malé rozměry nebo je jinak skryt, tak uživatel nic nepozná).

Obranou proti této formě útoku je veškeré akce provádět metodou POST a nikoliv GET a ve skriptech používat výhradně $_POST a ne třeba $_REQUEST. Moc platné nám to ale nebude, protože poslání dat metodou POST o moc složitější není:

<form action="http://www.example.com/" method="post">
<input type="hidden" name="action" value="delete" />
</form>
<script type="text/javascript">
document.getElementsByTagName('form')[0].submit();
</script>

Pokud tento kód umístíme do neviditelného iframe, uživatel se opět nic nedozví.

Kontrola Refereru

Obrana tedy musí vypadat jinak. Jednou možností je kontrolovat hlavičku Referer. Tato hlavička jde samozřejmě podvrhnout (v PHP např. funkcemi fsockopen nebo pomocí kontextů), ale musíme si uvědomit, že nedílnou součástí útoku je to, že se vede přes uživatelův prohlížeč – jinak se nepošle autorizační cookie. A pokud by tato hlavička šla podvrhnout v prohlížeči, jedná se o jeho významnou chybu. Nevýhoda této obrany spočívá v tom, že odstřihneme uživatele, kteří mají posílání této hlavičky zakázané (nejčastěji kvůli ochraně soukromí). Druhá nevýhoda tkví v tom, že do naší aplikace znemožníme všechny přístupy zvenku – včetně těch oprávněných. Já mám např. ve zvyku posílat si e-maily, které mě informují o vložení diskusního příspěvku. Tyto zprávy obsahují přímý odkaz pro smazání nebo editaci příspěvku (po nezbytném přihlášení). Vyhovuje mi, že se nemusím přihlašovat vždy znovu, což by obrana pomocí kontroly Refereru znemožnila. Pokud navíc z administračního rozhraní vedou odkazy na neprověřené externí stránky, je potřeba z nich Referer odfiltrovat, protože pokud na externí stránce bude přesměrování hlavičkou Location, předá se původní Referer a kontrola tedy projde.

Při používání session proměnných PHP je možné zapnout direktivu session.referer_check, která Referer bude kontrolovat za nás. Tato direktiva ale kontroluje jakoukoliv část URL, takže ji může útočník snadno obalamutit.

Autorizační token

Implementačně složitější, ale také méně restriktivní je obrana pomocí generování autorizačního tokenu. Před provedením každé operace si vygenerujeme náhodný řetězec a při jejím provedení tento řetězec zkontrolujeme. Tuto techniku popisuje Chris Shiflett, ale jeho přístup znemožňuje používání aplikace ve více oknech najednou. Řešením je autorizační token generovat až v momentě, kdy platnost předchozího vyprší, nebo generovat nový token pro každý požadavek. Jeho ukládáním do databáze místo do session proměnné také umožníme oprávněný vstup do aplikace zvenku:

<?php
$token = rand_chars();

// při posílání odkazu e-mailem
mysql_query("INSERT INTO auth_tokens (token, validity) VALUES ('$token', NOW() + INTERVAL 1 DAY)");
$url = "http://$_SERVER[SERVER_NAME]/admin/detail.php?id=$id&token=$token";

// při výpisu formuláře pro editaci nebo smazání
mysql_query("INSERT INTO auth_tokens (token, validity) VALUES ('$token', NOW() + INTERVAL 1 HOUR)");
echo "<input type='hidden' name='token' value='$token' />\n";

// při provedení akce
mysql_query("DELETE FROM auth_tokens WHERE validity < NOW()"); // smazání zastaralých
mysql_query("DELETE FROM auth_tokens WHERE token = '" . mysql_real_escape_string($_REQUEST["token"]) . "'"); // smazání použitého
if (mysql_affected_rows()) {
    // aktualizace záznamu
}
?>

Viz rand_chars.

Jednodušším typem této obrany je vygenerovat jediný, trvale platný token pro každého uživatele.

Používání autorizačního parametru v URL

Pokud autorizační údaje nebudeme uchovávat v cookie, ale v URL, útok podstatně znesnadníme, protože útočník bude muset tyto údaje nejprve nějak zjistit. Problém je v tom, že se dají zjistit na řadě míst – v historii prohlížeče, na proxy serverech, v logu webového serveru, kvůli hlavičce Referer se dostanou i na servery, které uživatel navštíví odkazem na našich stránkách. Nejvážnější riziko předávání URL na jiné servery v hlavičce Referer můžeme sice potlačit, ale dá to dost práce: Všechny externí odkazy musí jít přes speciální stránku bez autorizačních údajů, která provede přesměrování. Toto přesměrování navíc nemůže být realizováno hlavičkou Location, která Referer předává, ale nestandardní hlavičkou Refresh. Zakázat musíme i přímé používání obrázků mimo naši aplikaci nebo počítadla přístupů.

Musíme vzít ale v potaz, že jakmile útočník autorizační parametr přímo získá, otevírá se mu mnohem více možností – přestane tápat a do aplikace se jednoduše přihlásí a kromě pohodlného provádění jakýchkoliv akcí získá přístup i ke všem datům. Proto bych tuto obranu spíše nedoporučoval. Samozřejmě je možné ji ale zkombinovat s používáním cookies a pro provedení akce požadovat jak cookie, tak parametr v URL.

Obrana na straně uživatele

Pokud se přihlašujete k aplikaci, o které nevíte, jestli je proti tomuto útoku zabezpečena, můžete se bránit i sami. Pokud se používají autorizační cookies platné do konce práce s prohlížečem, stačí aplikaci otevřít do jiné instance prohlížeče. V Internet Exploreru stačí spustit prohlížeč znovu, ve Firefoxu opětovné spuštění pouze otevře nové okno, které cookies sdílí s původním oknem, a nepomůže ani pokus o spuštění pod jiným profilem (přinejmenším pod Windows). Řešením může být spuštění pod jiným uživatelem operačního systému.

Aplikace, ve kterých se informace o přihlášenosti pamatují i mezi spuštěními prohlížeče, jsou samozřejmě mnohem zranitelnější. Od aplikace, kterou už nehodláme v nejbližší době používat, je samozřejmě vhodné se odhlásit. Ve vlastní aplikaci je také vhodné autorizační cookie po nějaké době zneplatnit. Čím kratší je tato doba, tím se riziko útoku zmenšuje, ale zhoršuje se také pohodlí práce s aplikací – málokomu se chce znovu přihlašovat po pěti minutách nečinnosti. Za rozumný kompromis považuji jednu hodinu.

Zvláštnost tohoto útoku spočívá v tom, že je na serveru možné provádět akce (jako např. mazat záznamy), ale není možné data číst. Čtení dat z jiných domén pomocí JavaScriptu by totiž měl zakázat prohlížeč – a skutečně to dělá přinejmenším IE 6 SP2 a Firefox 1.5 (a možná i starší verze a další prohlížeče). Aplikace se ale samozřejmě dá napsat tak špatně, aby bylo možné i čtení dat – stačí na server umístit soubor s JavaScriptem, který bude ve funkci nebo proměnné obsahovat data, a k těmto datům se útočník obvykle dostane také. Řešením je do externího JavaScriptu žádná data neukládat a stahovat je např. AJAXem. Tento útok je také další důvod, proč při změně hesla uživatele požadovat heslo původní.

Přijďte si o tomto tématu popovídat na školení Bezpečnost PHP aplikací.

Jakub Vrána, Výuka, 24.4.2006, diskuse: 79 (nové: 0)

Diskuse

pangi:

Pekny clanok. Okdaz "rand_chars" je nefunkcny.

ikona Jakub Vrána OpenID:

Já vím. Odkazovaný článek už měl dávno vyjít, ale na Rootu, kde primárně vyjde, se to bohužel pozdrželo. Takhle ta funkce vypadá:

<?php
/** Vygenerování náhodného řetězce
* @param int [$count] délka vráceného řetězce
* @param int [$chars] použité znaky: <=10 číslice, <=36 +malá písmena, <=62 +velká písmena
* @return string náhodný řetězec
* @copyright Jakub Vrána, http://php.vrana.cz
*/
function rand_chars($count = 8, $chars = 36) {
    $return = "";
    for ($i=0; $i < $count; $i++) {
        $rand = rand(0, $chars - 1);
        $return .= chr($rand + ($rand < 10 ? ord('0') : ($rand < 36 ? ord('a') - 10 : ord('A') - 36)));
    }
    return $return;
}
?>

peCan:

Ja bych to řešil myslím jednodušeji to rand_chars();
asi takto nejak:
$rand_chars=md5(microtime().rand(0,999));

kratsi, ne?

n4sa:

...ale md5 obsahuje jen malé znaky, ne? V tom je trochu rozdíl oproti původní funkci..

n4sa:

jo a dokonce jen hexadecimální znaky.
Ale je fakt, že v tomto případě a při tomto použití to úplně nehraje zas takovou roli.

ikona Jakub Vrána OpenID:

Obecně to nejde, protože když znám přesný čas generování, tak má jakýkoliv výstup jen 1000 možností. Když znám alespoň přibližný čas, tak je těch možností jen několik tisíc. A třeba u diskusních příspěvků se obvykle ukládá čas s přesností na vteřiny.

Láďa K.:

Velmi čtivý článek, avšak způsobující poněkud depresivní náladu, jakoby snad proti tomuto druhu útoku ani nebylo spolehlibé obrany. :)

Martin:

V případě, že nepoužívat https, tak žádná ochrana opravdu neexistuje :-)

Leo:

Hezke, zase je s cim experimentovat :-) Leo

v6ak:

*Měl bych ještě jednu věc proti předávání tokenu přes URL: Myslím, že útočník by mohl přečíst adresu okna, které otevřel.

*Firefox lze spustit vícekrát, vizte na forum.czilla.cz téma Dvě instance aplikace (WAS:sessions)

ikona Jakub Vrána OpenID:

Díky za informaci. Přímý odkaz je http://forum.czilla.cz/viewtopic.php?t=7207.

ikona v6ak:

Před chvílí jsem sem někoho odkazoval a všiml jsem si jedné chyby:
"ve Firefoxu ... nepomůže ani pokus o spuštění pod jiným profilem (přinejmenším pod Windows). Řešením může být spuštění pod jiným uživatelem operačního systému."
To není pravda, to nepůjde. Skutečně se musí použít proměnná MOZ_NO_REMOTE nebo parametr. (Parametr se jmenuje -noremote nebo -no-remote, spíš to druhý.)

ikona Jakub Vrana OpenID:

Proč by to nešlo?

ikona v6ak:

Zkus to, bude se to tvářit, jako bys to spouštěl pod stejným uživatelem.

ikona Jakub Vrana OpenID:

Já to právě dělám úplně běžně. Na domácím počítači je obvykle přihlášeno víc lidí (pomocí funkce Přepnout uživatele), každý má spuštěný svůj Firefox a do zelí si samozřejmě nijak nelezou.

ikona v6ak:

Aha. Já jsem používal runas a tam problém byl. Přepínání uživatelů je ale asi paměťově náročnější a nejlepší asi stejně bude runas + -no-remote. Záleží teda taky na HW, někde na tom nezáleží.

Chytrak:

Nejjednodussi je preci kontrolovat jestli se nezmenila IP adresa (nebo verze prohlizece). Pokud ano, potom pripadneho utocnika proste nepustit, ani kdyz ma napriklad ukradenou cookie. Nekterym uzivatelum se muze IP menit, takze staci male nastavovatko at si kazdy urci zda chce tuto moznost kontroly mit. Kontrolu unikatniho retezce, ktery se generuje po kazdem kliknuti, lze take pouzit. Staci hlidat jen v nekterych castech webu, napriklad pri zminovanem mazani atd.

Martin Straka:

Kontrola IP ti u CSRF útoku nepomůže. Podstata je v tom, že ten požadavek provede prohlížeč oběti, tedy všechno (včetně té IP adresy) je stejné jako u legitimního požadavku a bez speciální obrany to nemůžeš rozlišit.

ondra:

A heslo na bloh llaika?;)

ikona Jakub Vrána OpenID:

To se musíte zeptat jeho, ale je to tam už dost dlouho.

Llaik:

guest/guest :)

kazdopadne zminovany clanek jiz (bohuzel/bohudik/...) neexistuje. Faktem je, ze v konecnem dusledku jsem uznal zminovane argumenty jako opravnene :)

XSRF utoky maji pred pochopenim jeden zakladni problem - je tezke si je predstavit. Neni to utok, kde by klient byl extra aktivni, ani utok, kde by klient nebyl. On provede akci, se svou IP a cookies, ale neni to zase schopen bezmezne upravit... rozhodne je to jeden z "nejzabavnejsich"  jednoduchych utoku.

Premyslim o pokusu o prelozeni clanku o ruznych typech utoku na webove aplikace, protoze to muze nekterym zacinajicim anglictinou-nevladnoucim programatorum jiste pomoci. Nema ale imho smysl psat nic vlastniho, protoze takova veda to zase neni, tj. co mohlo byt napsano, napsano uz povetsinou bylo. Alespon v jinem, nez ceskem jazyce.

Pro anglicke ctenare: http://llaik.rivil.com/?p=82

Jan Abruzzi:

Vetsina jednoduchych utoku je v cestine vysvetlena napriklad zde: http://stoyan.ic.cz/hacking/

Alfi:

1. většinou stačí nezveřejňovat administrační rozhraní na veřejně známé doméně (www.server.cz -> admin1.server2.cz) - počet možných útočníků se tak omezí na ty, kteří ji znají, příp. kteří se ji náhodou dozví (log) nebo se možné útoky omezí pouze na ty, které může udělat běžný uživatel na www.server.cz (vkládání komentářů apod)

2. kdo se hodně "bojí", většina prohlížečů má volbu "accept images from the originating server only" a "disable javascript" :-)

error414:

to skoleni je moc dobre, nemel jsem ucelenou predstavu o moznostech utoku na PHP scripty a tohle skoleni me to ucelilo.

Ale az tak moc ze kdyz jsem jel domu autobusem do brna mohl jsem zautocit na jakykoliv nizkourovnovy blog ktery si pisi mene zkuseni programatori.

Smutne zjisteni.

Leo:

Otazka je, jestli nejde klientskym JS podvrhnout hlavicka Referrer, napriklad AJAX umi hlavicky pozadavku pridavat, cetl jsem tohle jen zbezne, ale zda se, ze jde pridat a menit i tuhle:

http://www.cgisecurity.com/lib/XmlHTTPRequest.shtml

Leo

ikona Jakub Vrána OpenID:

Jak je v článku uvedeno, čtení dat (obecně kladení požadavků na ně) z jiných domén zakazuje prohlížeč. Takže riziko podstrčení hlaviček AJAXem naštěstí nehrozí.

Martin Straka:

Ano, postup tam popsany ale potrebuje jeste proxy server, ktery z toho udela dva nezavisle pozadavky (a vsechny to neudelaji). Asi by s tim pozavakem ale nesla cookie, kterou pro CSRF utok potrebujes.

XmlHTTPRequest neudelas na jinou domenu, nez odkud je script spusteny. A pokud uz ho jsi (jako utocnik) schopny ve stejne domene spustit, tak je tam XSS chyba. A pokud mas XSS chybu, tak je ochrana proti CSRF (treba ta s tokenem) neucinna:)

Potom je/bylo take zajimave vyuziti flash playeru (starsich verzi), u kterych obcas slo prekonat cross-domain omezeni.

Leo:

Tak jsem si s tim trochu hral a asi je to nastesti opravdu nepouzitelny... Leo

Martin Straka:

a jeste prakticky priklad k tomu, jak Jakub psal:

"Aplikace se ale samozřejmě dá napsat tak špatně, aby bylo možné i čtení dat - stačí na server umístit soubor s JavaScriptem, který bude ve funkci nebo proměnné obsahovat data, a k těmto datům se útočník obvykle dostane také. Řešením je do externího JavaScriptu žádná data neukládat..."

http://www.webappsec.org/lists/websecurity/…/msg00087.html

error414:

A co do kazdeho formulare do hidden pole ukladat nahodny retezec, ktery bude mit take uzivatel v session. Utocnik nemuze nijak zjistit jaky retezec ma odeslat v podvrzenem formulari.
Taky proto ze se ted retezec bude menit.

ikona Jakub Vrána OpenID:

Ano, to se popisuje v části Autorizační token.

gulo:

Kde by som mohol najst nejaky manual ohladne PHP funkcie tokens(okrem php.net) alebo podobny spobob autorizacie.

Naco je ten token vlastne dobry.
Dik za odpoved.

ikona spaze:

Jakube, můžeš prosím detailněji vysvětlit následující:

/[Chrisův] přístup znemožňuje používání aplikace ve více oknech najednou. Řešením je autorizační token generovat až v momentě, kdy platnost předchozího vyprší, nebo generovat nový token pro každý požadavek./

tedy přesněji ty dvě řešení? A zároveň přidat vysvětlení /jediný, trvale platný token pro každého uživatele./

Zdá se mi, že první řešení (/generovat až v momentě, kdy platnost předchozího vyprší/) a poslední (/jediný, trvale platný token/) snižují obranu proti CSRF a ten prostření (/generovat nový token pro každý požadavek/) stejně nedovolí používat aplikaci ve více oknech. Ale možná se jen pletu a tak si to raději nechám vysvětlit ;)

A taky prosím přilož, co si představuješ pod pojmem "trvale platný token", mě to přijde jako token, který bude mít platnost po dobu sezení, ale není to z toho úplně jasný.

ikona Jakub Vrána OpenID:

Nový token pro každý požadavek - token se přidává do pole všech platných tokenů.

Nový token až po vypršení starého - ověří se, jestli už je nějaký token nastaven a stále platí. Pokud ano, tak se použije, jinak se vygeneruje nový.

Trvale platný token - při provedení akce se token neodnastavuje, platí po celou dobu sezení.

Co se snižování obrany týče, tak to je pravda - získání jedné stránky s tokenem dovolí provést i jiné operace.

Naopak ukládání tokenů do pole aplikaci ve více oknech používat dovolí.

mj41:

> Co se snižování obrany týče, tak to je pravda - získání
> jedné stránky s tokenem dovolí provést i jiné operace.

Může někdo získat stránku díky této formě útoku? Je to přece "request" forgery. :-)

Navíc všechny důležité aplikace samozřejmě všechny sezení vedou výhradně šifrovaně (https, SSL).

ikona v6ak:

Běžně to opravdu nepůjde, ledaže by's mohl přes parametr ovlivnit třeba začátek action. Svým způsobem by šlo pořád o CSRF.
Tady se asi myslí spíš získání jiným způsobem.

ikona Jakub Vrána OpenID:

Pomocí CSRF to skutečně získat nejde. Útočník se k tokenu může dostat třeba tak, že si oběť uloží na disk soubor s kódem stránky a pro pobavení ho pošle známému. Je to samozřejmě chyba oběti, ale používáním více tokenů se dají minimalizovat škody.

mj41:

Diky, doslo mi to. Tohle osetrovat nebudeme. Spoluprace s utocnikem "to uz jinde jsme, to je jina vesnice" :-).

Leo:

Doplnim, ze ve Firefoxu a spol. se uzivatel muze jeste branit nastavenim network.http.sendRefererHeader v about:config na hodnotu 0 nebo 1:

http://kb.mozillazine.org/Network.http.sendRefererHeader

Leo

ikona dgx:

Výhoda autorizačních tokenů je v jejich dočasné platnosti, takže jakýkoliv trvale platný token ztrácí na efektu.

Jakube, v tom navrhovaném řešení ti vypadlo propojení mezi tokenem a session ID - upravil bych tedy příkazy na INSERT INTO auth_tokens (token, validity, sessionID) apod.

Příklad: na blogovacím systému Nucleus/BlogCMS umožním přihlašování stálým čtenářům, kteří tak získají možnost editovat své komentáře (a samozřejmě nic víc). Sám budu jediný uživatel, který může psát, editovat a mazat články. Jenže autorizační token vygenerovaný pro editaci komentáře má stejnou sílu, jako token určený ke smazání článku. Tedy uživatel s nízkými právy si může v systému nechat vygenerovat tokeny, které použije pro útok.

ikona dgx:

Pro zmíněný případ, kdy si posíláš odkaz na smazání položky emailem, lze samozřejmě řešit i případ, kdy se do databáze uloží prázdné sessionId a při kontrole se bude ignorovat. Důležité je, že o tom, co se uloží do DB, rozhoduje čistě aplikace.

ikona dgx:

ad kontrola refererů:

Direktiva session.referer_check je na dvě věci, úplně na ni zapomeňte. Nastavíte-li

ini_set('session.referer_check', 'www.dgx.cz');

Pak přístup ze stránky http://evil.example.com?naive=www.dgx.cz stačí k jejímu ošálení. Vůbec bych ji v souvislosti s bezpečnostní nezmiňoval.

Kontrolovat referery musíme ručně a to analýzou prvních X znaků proměnné $_SERVER['HTTP_REFERER']. Ale ani to není samospasitelné. Dejme tomu, že budeme pro administraci blogu na www.dgx.cz referery vyžadovat. Pak útočník pošle do komentářů odkaz na stránku evil.example.com - admin při kontrole komentářů na odkaz klidne. Na uvedené stránce se však nachází jen přesměrování

Location: http://www.dgx.cz/blog.php?action=delete&item=10

Hlavička Location přenese původní referer a tato kontrola selže. Navíc je možné adresu v Location ještě doplnit o autorizační token získaný z refereru.

ikona Jakub Vrána OpenID:

Díky, článek jsem o tyto informace doplnil.

pojízdná kočka:

škoda, že se to kontroluje takto obecně; podle mě by (bývalo) bylo lepší, pokud by se kontrolovalo, zda adresa *začíná* na referer_check.. :-(

Tomas:

Jen bych dodal, ze referer se da osalit jako kterykoli jiny parametr ala cookies - ve FF to zjednodusuje napr rozsireni refcontrol.

ikona dgx:

Abych uzavřel svůj monolog... ;)

- můžeme uživateli podstrčit své sessionID (direktiva session.use_only_cookies = 1 to znesnadní, ale sama tomu nezabrání).
- můžeme ho nechat vykonat operaci z jeho vlastního prohlížeče s přihlášeným session (CSRF)
- můžeme ho oklamat referer v případě požadavku GET (hlavička Location)
- obecně ale nelze spoléhat na referer vůbec (uživatel ho může v prohlížeči potlačit, útok může být veden i přes fsockopen)
- můžeme si v jiné části rozhraní předgenerovat univerzální autorizační tokeny

Co z toho vyplývá? Kromě sessionID je potřeba kontrolovat ještě jeden údaj, který nemůže útočník zjistit ani fingovat. Říkejme mu také autorizační token.

Tento token by měl hodnotu sha1(sessionID + user_password). Bude se přenášet jen přes skryté pole formuláře (tedy používat POST. Šlo by v nejhorším použít i URL a po předání přesměrovat na adresu bez tohoto tokenu, ale raději ne).

Token má platnost jen po dobu platnosti sessionID a k jeho fingování je nutné znát heslo uživatele (no, nebo hash). Není třeba ho ukládat do databáze.

Situaci lze ještě zkomplikovat tím, že by token byl určen jen pro konkrétní operaci. Tedy sha1(sessionID + user_password + 'delete_comment|123')

pojízdná kočka:

Díky za podnětný monolog :-)
Mě tedy z toho vyplývá docela tristní situace - jakmile uživatel CSRF-imunního webu vstoupí do nějaké zóny, která je pro útočníka zajímavá (správa účtu registrovaného uživatele, administrace) je potřeba *každý* odkaz předělat na HTTP-POST formulář s autorizačním tokenem. Uživatelovo heslo, které jej tvoří, je nutno číst z databáze při *každém* znovunačtení stránky. Pochopila jsem to správně?

ikona Jakub Vrána OpenID:

Ne. POSTem s tokenem je potřeba přistupovat pouze ke skriptům měnícím stav aplikace (např. smazání objektu) – to by ale měl být POST tak jako tak.

Token navíc rozhodně nemá být heslo ale nějaký náhodný řetězec.

pojízdná kočka:

Dík za vysvětlení

Honza Odvárko:

Trošku pozdě, ale přece bych se rád zeptal... jestli dobře chápu, tenhle útok je vyloučen pokud se důsledně ošetřuje výstup např. fcí htmlspecialchars(), a řetězce generované do JavaScriptu se ošetří vlastní fcí na znaky '" apod. Chápu správně, že v takovém případě je CSRF nemožný?

ikona Jakub Vrána OpenID:

Bohužel to chápeš špatně. Síla CSRF spočívá v tom, že záškodnický kód může být na jekémkoliv serveru. Pokud se ho útočníkovi podaří umístit přímo na tvůj server (nejčastěji pomocí XSS), je to pro něj samozřejmě výhoda (protože má větší šanci, že tam přijde přihlášený uživatel), ale není to nutná podmínka.

ikona Medhi:

Používám tuto ochranu ve službách BlueBoard.cz. Dokáže zabránit téměř 90% "komentářového spamu" (na případu BlueBoard.cz rozumějte spamu do návštěvních knih apod).

Máme ale trápení s tím, že je to velmi náročné na DB. Možná dělám něco špatně. Místo INSERT INTO, používám REPLACE, kdyby náhodou token se stejným řetězcem již existoval. Řetězec mám totiž kratší, asi 10 znakový.

BlueBoard.cz je velmi používaná věc a v tabulce je tokenů na statisíce. Výsledkem je velký nápor na disky serveru, které velmi často zapisují. Častěji než čtou.

Zkoušeli jsme různé typy tabulek, ale nebyl v tom velký rozdíl.

Lze něco udělat lépe, jinak? Děkuji

ikona Jakub Vrána OpenID:

Pokud používáš session a pokud se do aplikace nepřistupuje zvenku (takže uložení do databáze je vždy předcházeno zobrazením formuláře), tak lze tokeny ukládat do session proměnné. Aby jich mohlo být aktivních více najednou, lze použít kód <?php $_SESSION["tokens"][$token] = true; ?> a následné testování <?php isset($_SESSION["tokens"][$token]); ?>

liquid:

REPLACE je ideální vůbec nepoužívat na takhle zátěžovou věc.
Když uděláš DELETE a pak INSERT tak je to mnohem rychlejší než jeden REPLACE.

m4tty:

IMHO otázka byla spíše mířena na méně zapisování na disk, ale ve výsledku to bude znamenat pouze to, že se přidá jeden zápis na disk navíc :)

ikona ehmo:

ani jedna z popisovanych ochran nefunguje. vsetky su prekonane a CSRF je prakticky jeden z najhorsich moznych utokov, pretoze je nepotlacitelny. jedna sa totizto o vlastnost priamo HTTP (rfc 2616). tokeny su prekonane uz davno, tu je napriklad postaveny chat na csrf a tokenoch
http://www.thespanner.co.uk/2008/02/11/csrf-chat/

csrf nie je mozne osetrovat ani dobrym kodom, ani ziadnymi pomockami, aj ked par sikovnych projektov pre ochranu proti CSRF vzniklo (owasp)

ikona Jakub Vrána OpenID:

CSS overlay je jistě zajímavá technika, ale říkat kvůli tomu, že tokeny jsou překonané a CSRF je nepotlačitelný, je hodně přehnané.

Obzvlášť když se mu dá bránit technikou starou jako rámy samy: if (self != top) top.location.href = location.href;

Doplnění je to dobré, ale ten FUD okolo snad být nemusel...

ikona ehmo:

troska literatury
http://www.xml.com/pub/a/2006/06/28/flashxmlhttprequest-…-rescue.html
http://www.webappsec.org/lists/websecurity/…/msg00069.html
http://www.webappsec.org/lists/websecurity/…/msg00157.html
http://blogs.zdnet.com/security/?p=946

technik ako obist csrf je 3x tolko, ako jednotlivych ochran. zatial velmi dobre funguje
http://www.owasp.org/index.php/CSRFGuard
ale tiez nie je 100%. na kazdu ochranu existuje technika a nateraz su tokeny fajn pre odstranenie 98% ludi. ci sa casom najde sposob, ktorym bude mozne csrf potlacit uplne, to sa uvidi

ikona Jakub Vrána OpenID:

Literatura je zajímavá, ale přímou souvislost s problematikou nebo dokonce protiútok k popisované obraně nevidím.

1. Pokud na web umístím soubor crossdomain.xml s odpovídajícím obsahem, jsou možné cross-domain requesty.

2. Obrana pomocí hlavičky Referer je nedostatečná. To tvrdí i tento článek.

3. Jakási historická verze IE dovoluje stáhnout cizí stránku, už téměř rok je ale k dispozici záplata: http://secunia.com/advisories/19738/.

4. Neopravená verze Javy dovoluje stáhnout cizí stránku. Nejsem si jist, že se z Javy pošlou cookies prohlížeče, ale ať tak či onak, záplata je k dispozici: http://sunsolve.sun.com/search/document.do?…-66-233324-1.

Opět jde bohužel pouze o šíření FUDu. Uvádíš sice související odkazy, ale závěry, které z nich vyvozuješ, jsou nepřiměřené.

Yawgmoth:

co se týče refererů, není lepší kontrolovat pouze přítomnost "špatného" refereru místo přítomnosti "správného"?

Někdo má referer zablokovaný, nebo někdo přistupuje jako ty Jakube na stránku z mailu (pokud se tedy nejedná o webové rozhraní posílající špatnou hlavičku), ale prázdný referer nám nevadí, stejně jako referer typu REFERER_BLOCKED a podobné které jsem již zahlédl. Vadí jen hodnota tvaru http://jiná_doména_než_naše/ kterou každý rozumný prohlížeč oběti tohoto typu útoku odešle.

Potom stránka funguje všem mimo lidí co si schválně nastaví referera na reálnou ale jinou adresu (což by pro mne bylo nepochopitelné, jestli to někdo dělá) a zranitelní zůstávají jen ti uživatelé, kteří mají referera zablokovaného, což je snad celkem malá skupina a bohužel bych skoro řekl, že to je jejich chyba, ti si prostě musejí dávat pozor na co klikají .. můžeme je na to někde varovat .. ale alespoň jim to funguje (tedy není to řešení vhodné pro banku, nicméně nějaký zábavní server, s hrou, chatem a já nevím čím, by s takovou slabinou fungovat mohl)

ještě je tu problém s přesměrováním, ale ten se dá vyřešit jak je v článku psáno, všechny vkládané odkazy z našich stránek ven půjdou přes skript který nastaví referera na adresu kam přesměrováváme (prázdný řetězec by nestačil když ho náš filtr propouští)


tokeny jsou fajn, ale někdy se člověk dostane k velké aplikaci a nechce se mu všechno prolézat a vybavovat veškeré postihnutelné odkazy a formuláře tokenem, takhle uděláme jeden skript na sotva pár řádek na kontrolu referera který se bude vykonávat všude a hotovo..

ikona v6ak:

"zranitelní zůstávají jen ti uživatelé, kteří mají referera zablokovaného, což je snad celkem malá skupina a bohužel bych skoro řekl, že to je jejich chyba, ti si prostě musejí dávat pozor na co klikají .."

Spíš bych je upozornil, že si jej mají zapnout, protože bez toho nic neudělají.

BTW: Blokovat každý referer je IMHO blbost, ale co když si uživatel blokuje jen cross-site referery aby zabránil poslání tokenu z adresy? To by si pak zároveň blokoval právě ty referery, které potřebujeme. A tedy bychom v takovém případě neměli akci provést...

ikona Jakub Vrana OpenID:

Bez Referera by nic neudělali v postupu popsaném v článku. Yawgmoth navrhuje jiný postup, kde by uživatelé s vypnutým posíláním Referera mohli svobodně pracovat, ale byli by zranitelní na CSRF. Proto tento postup považuji za nevhodný.

ikona v6ak:

Já to chápu a taky to považuji za nevhodné. Ze stejného důvodu.

ikona Jakub Vrana OpenID:

Lidi (a některé firewally) vypínají posílání Refereru kvůli zvýšení bezpečnosti. Bylo by nešťastné právě těmto lidem bezpečnost snížit.

Referera si nemůžeš nastavovat dle libosti, jeho hodnotu určuje prohlížeč. Externí odkazy by tedy musely vést na stránku na našem serveru, jejíž adresa by ale byla na black-listu.

fiso:

> Čtení dat z jiných domén pomocí JavaScriptu by totiž měl zakázat prohlížeč

Týka sa toto iba AJAXu, alebo je možné umiestniť do škodlivého kódu iframe so zdrojom na útočenú stránku? Potom by sa dalo cez DOM zistiť, aký autorizačný token sa zrovna použil a teda prispôsobiť tomu svoj podvrhnutý formulár. Prípadne cez DOM vyplniť formulár stránky v iframe a tam ho poslať. To by už bol sakra problém!

ikona v6ak:

NN, na iframe s cizí stránkou si nešáhneš.

ikona 3wl4k:

Trochu som sa v tom vrtal a ako vhodna ochrana mi pride generovanie samostatneho tokenu pre kazdu akciu s obmedzenou platnostou, len pre jedneho uzivatela a pre jedno session, napr. cez tabulku:

-- Table authorization_tokens
CREATE TABLE `authorization_tokens`
(
  `user_id` Int UNSIGNED,
  `validity` Datetime
  COMMENT 'When token expires (default life time: 1hour)',
  `action` Varchar(25)
  COMMENT 'Eg. delete_news, or update_article',
  `action_data` Varchar(255)
  COMMENT 'Eg. id=4',
  `token` Char(20)
  COMMENT 'RAND CHAR'
);
CREATE INDEX `validity` ON `authorization_tokens` (`validity`);
CREATE INDEX `action` ON `authorization_tokens` (`action`);

Kde by sa po prihlaseni/odhlasenie uzivatela vsetky jeho tokeny zmazali (a teda autrizacny token by mal relativne kratku platnost), dalej by sa na kazdu akciu dal pouzit len raz (pseudokod):
if( tokens_match()){
  update_article();
  regenerate_token();
}

Neviem si predstavit, ako by utocnik pomocou CSRF ziskal aj session ID a aj hodinu platny autorizacny token pre kazdy akciu (aj ked proti XSS no neviem...)

Jediny problem zostava v tom, co sas stane ak user napriklad zvoli, ze ide upravovat clanok, potom na chvilku odskoci a ked sa vrati tak najskor prida dve/tri novinky a kym sa dostane k tomu aby stlacil "save", tak token vyprsi...

Napadlo ma riesit to
a) pomocou ajaxu - co mi nepride dobra alternativa, kvoli XSS
b) pri neuspechu kontroly tokenu vypisat celu stranku este raz, tentokrat s novym tokenom a samozreme s datami tazko natukanymi od uzivatela... (mozno by este bolo zaujimave pridat tam niekde take mile male tlacitko na vypisanie povodnych dat)...

Je taketo riesenie bezpecne a vobec, ma zmysel?

Jan Pejša:

Pokud je uživatel přihlášen (používá se sessionId uložená v cookies), má povolený javascript a komunikuje se šifrovaně (https), pak se může "autorizační token" uložit také do cookies.

Výhoda tohoto řešení je, že hodnota cookies se dá přečíst v javascriptu (pokud nebyla cookie vytvořena s parametrem HttpOnly). Webová aplikace vždy před odesláním formuláře přečte hodnotu tokenu a dodá ho jako další položku formuláře - potom ho včetně "tokenu" odešle. Útočník nemá šanci zjistit hodnotu tokenu (pokud se používá šifrovaná komunikace) a proto útočník nemůže sestavit formulář se správnými daty, aby ho webová aplikace přijala.

Někde jsem dokonce viděl řešení nebo doporučení používat hodnotu sessionId jako autorizační token. Pokud by totiž útočník zjistil sessionId, může se rovnou do aplikace přihlásit a nemusí útočit přes CSRF.

Hrach:

To je dle mě velmi dobrá a důležitá poznámka. Proč vymíšlet další způsob generátoru, který by měl být nezávislý na všem možném. To session je opravdu celkem jednoduché a učinné řešení.

pojízdná kočka:

Zdravím,
Možná je trochu hloupá otázka, ale přecijen...
Dejme tomu, že jsem někomu udělal např. nějaké (jakékoli) administrační rozhraní, které má standardní přístup pro administrátory (např. jako je u adminera, tzn. přihlašovací formulář HTTP-POST, heslo v databázi a checknutí administračních práv přes session).
Když klientovi do dokumentace napíšu, že po dobu, co "administruje", nemá v prohlížeči otvírat jiná okna, je toto dostatečná prevence, která CSRF řeší?
(jenom mě zajímá odpověď na tuto otázku, jinak chápu, že smyslem článku je takto uživatele neomezovat)
Díky

ikona Jakub Vrána OpenID:

Teoreticky to stačí, v praxi se tím uživatelé nejspíš nebudou řídit.

paranoiq:

myslím, že teoreticky nestačí ani to. uživatel totiž nemusí během práce stránku ve vedlejším okně otevřít sám. může to za něj udělat http refresh nebo javascript. musel by ostatní okna zavřít

Wan-To:

Možná jsem na něco zapomněl, ale napadá mě celkem jednoduchá obrana proti CSRF: Webovou aplikace důsledně rozdělím na view a controller části. (View skript nic neprovádí, pouze zobrazuje, tudíž je neškodný, oproti tomu controller nic nezobrazuje, ale zase pouze něco vykonává, a po vykonání se hned přesměruje zpět na nějaký view - kritická část.) Na začátku každého controlleru, před voláním session_start(), nastavím ID relace na základě GET parametru - session_id($_GET["session_id"]) - a ve všech viewech přidám do odkazů a formulářů vedoucích na controllery GET parametr session_id, do kterého zapíšu ID relace. Toť vše. View komponenty jsou neškodné, ty si mohou vesele tahat ID relace z cookies. Controllery v případě CSRF útoku ale ID relace nikdy nezískají, takže ani neprovedou očekávanou akci.

ikona Jakub Vrána OpenID:

Neboli token nastavíš na session ID. To je funkční řešení, jeho problém je ale v tom, že tím se token stává příliš cenný – při jeho získání by se mohl útočník začít vydávat za daného uživatele. Token může uniknout třeba tak, že si uživatel uloží kód stránky a někomu ho pošle e-mailem. To je sice chyba uživatele, ale vysvětlujte jim to.

Proto je vhodné, aby token byl nezávislý na session ID.

zelenomodrypes:

Neznemožňuje náhodné generování tokenů pro každou stránku používat tlačítko Zpět v prohlížeči?

Napadlo mě ještě jedno řešení, možná už to tu někdo zmiňoval - přidávat do citlivých odkazů (přidávání/editace/mazání záznamů) nějaký hash události(název stránky + id události/záznamu + nějaký další řetězec). Tyto hashe není třeba navíc nikam ukládat. Řetězec přidávaný do hashe se může jednorázově generovat při přihlášení uživatele. Možná si ale neuvědomuji nějaká další bezpečnostní rizika.

pojízdná kočka:

Jak řekl Jakub výše, tak se ČSFR... ééé ...CSRF má týkat jen akcí, které něco na stránkách mění.

A podle mě je přecházení zpět v historii prohlížení stejné jako bez tokenů - tzn. když pracuješ v administraci, něco upravíš, něco vymažeš, atd. a když potom jdeš v historii stránek zpátky, tak se tě u POST požadavků bude prohlížeč ptát, jestli chceš POST data znovu poslat. Podle mě nedává moc smyslu něco takového dělat nebo ty požadavky potvrzovat, protože často ani nevíš, co si tím přivodíš - ať už ta administrace opatření proti CSRF má nebo nemá.

K druhé části - já bych řekla, že by to tvoje řešení i šlo - v podstatě jsi ten token toliko udělal permanentní po dobu přihlášení (místo: pro každou akci nový). Ale zase pozor, kam ho budeš ukládat (cookie, session, databáze) a kde všude se bude objevovat. Přihlášen můžeš být hodiny i dny, po které se případný útočník může snažit token zjistit - u změny tokenu před každou akcí se naopak šance tohoto šťourala na úspěch snižují.

pojízdná kočka:

Tak jsem si to i zkusila (hned jsem to nasadila do jednoho admina) a zdá se, že to fachčí.
* po úspěšném přihlášení do adminu naplním $_SESSION['csrf_token'] náhodným číslem
* na vrácení skrytého formulářového políčka (s hodnotou zahašovaného csrf_tokenu s nabídnutým parametrem (jako ten vybírám krátký řetězec, který charakterizuje danou agendu a je stejný pro všechna tlačítka v formuláři)) - na to jsem si napsala funkci, kterou zavolám uvnitř formuláře v adminu
* na kontrolu tokenu mám druhou funkci, která vrací shodu/neshodu a v případě neshody rovnou nastavuje do session chybovou hlášku - tuto funkci pak můžu použít rovnou ve zpracování jako operátor: if($_POST["zaznam_upravit"] && csrf_ok("smluveny-kod")){
//zpracování
}

Takže díky za ten nápad - jeden z mála jednodušších řešení, než dokázal vymyslet Jakub ;-)

p.s. Myslím, že by nebylo složité něco takového implementovat do Nette\Forms, (ale on už něco takového asi bude stejně mít).

wizard:

Lze tedy vůbec bránit operace provozované pomocí GET?

Pokud má aplikace logiku typu: example.com/admin/articles/delete/3 nebo /index.php?action=delete&article_id=3

ikona v6ak:

Lze se bránit těmto operacím přes GET, to bude asi nejlepší.

Jinak je možné použít referer nebo token v adrese, oboje ovšem přináší určité problémy (token - ukradení přes referer; referer - možnost vypnout, asi vypnutí při https, zneužití obrázku na diskusním fóru), se kterými je potřeba počítat.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.