Automatická obrana proti CSRF

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

O útoku CSRF jsem již podrobně psal. Jde v něm o to, že útočník provede z cizí stránky nějakou operaci jménem přihlášeného uživatele, aniž by o tom tento uživatel věděl. Nejlepší obranou proti tomuto útoku je generování a kontrolování tokenů.

Ruční generování a kontrolování tokenů je nudná práce, na kterou člověk navíc může snadno zapomenout, celá činnost se dá ale automatizovat. Využijeme k tomu funkci ob_start, které předáme funkci hledající formuláře odesílané metodou POST a doplňující k nim skryté pole s tokenem. Druhá část ochrany bude tento token kontrolovat.

<?php
function csrf_token($s) {
    $hidden = "<div style='display: none;'><input type='hidden' name='csrf_token' value='$_SESSION[csrf_token]' /></div>";
    return preg_replace('~<form\\s+(?![^>]*\\baction=[\'"]?(?:https?:|//))[^>]*\\bmethod=[\'"]?post[^>]*>~i', "\\0\n$hidden", $s);
}

function csrf_protection() {
    if (session_id()) {
        if (!$_SESSION["csrf_token"]) {
            $_SESSION["csrf_token"] = rand(1, 1e9);
        }
        ob_start('csrf_token');
        if ($_POST && $_POST["csrf_token"] != $_SESSION["csrf_token"]) {
            return false;
        }
    }
    return true;
}
?>

Funkce csrf_protection nejprve otestuje, jestli je inicializovaná session (předpokládáme, že je uživatel identifikován právě nastavením session proměnných a ne třeba cookie), protože bez toho je obrana proti CSRF zbytečná. Pokud ano, tak se vygeneruje token (pokud ještě neexistuje) a zaregistruje se funkce doplňující token do skrytého formulářového pole (token se nedoplňuje u formulářů směřujících na cizí server). Když se zjistí, že byl odeslaný POST formulář, tak je ověřen token a v případě jeho nesouhlasu se vrátí false. Použití je jednoduché:

<?php
session_start();
$csrf_ok = csrf_protection();

if ($_POST && $csrf_ok) {
    // uložení dat
    header("Location: ."); // přesměrování na stránku s výpisem
    exit;
}
if (!$csrf_ok) {
    echo "Neplatný token, odešlete prosím formulář znovu.\n";
}
echo "<form action='' method='post'>\n";
echo "<input name='jmeno' value=\"" . htmlspecialchars($_POST["jmeno"]) . "\" />\n";
echo "<input type='submit' value='Uložit' />\n";
echo "</form>\n";
?>

Funkce csrf_protection se samozřejmě může volat ve společném kódu, stejně tak vypsání chybové hlášky může být společné (třeba ve funkci vypisující HTML hlavičku stránky). V kódu tak zbývá jen ověřit proměnnou $csrf_ok.

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

Jakub Vrána, Řešení problému, 28.1.2009, diskuse: 54 (nové: 0)

Diskuse

Rattus:

Podobne automaticke kontroly maji problem pokud mate otevrenych vice tabu s danymi strankami - protoze kazda stranka pouzije stejnou promenou pro token a tudiz to nefunguje spravne.

Nevim zda reseni pouzit url stranky ve jmenu session promene tokenu vyresi vsechny kolize, spis ne.

ikona Jakub Vrána OpenID:

Špatně sis kód prostudoval. Toto řešení nastavuje jenom jeden token pro celou aplikaci a tudíž správně funguje i ve více tabech. Řešení, které používá pro každý formulář jiný token, se dá taky udělat vícetabově (je to popsané na http://php.vrana.cz/cross-site-request-forgery.php) a s automatickou obranou zkombinovat.

majo:

lenze co ked bude na stranke napr. input s vyhladavanim, v tom pripade pri kazdej poziadavke sa meni token... 30 poziadaviek = 30 tokenov....

ikona Jakub Vrána OpenID:

Na vyhledávání token potřeba není. Ale obecně vzato je samozřejmě vhodné staré tokeny mazat.

majo:

...je to popsané na http://php.vrana.cz/cross-site-request-forgery.php) a s automatickou obranou zkombinovat.

myslel som tuto kombinaciu.., automaticky sa budu vytvarat ku form-u ktory je v aplikacii zobrazovany permanentne (napr. to vyhladavanie s method=post) nove tokeny, ak maju platnost hodinu, moze sa ich neprijemne zbierat (zalezi na aktivite uzivatela)...

a este drobnost: ked sa dokonci spracovanie dokumentu, tusim nastava mysql_close a az potom sa vykona ob_start(callback), cize sa treba asi znova pripojit na db (pre zapis noveho/novych tokenov)...

ikona Jakub Vrána OpenID:

Vyhledávací formulář je vhodné posílat metodou GET. Už třeba jen proto, aby se na výsledky dalo odkázat.

callback se volá před zavřením připojení k databázi.

Jiří Málek:

Já si třeba generuji pro každou akci zvlášť token. Když je na stránce 5 formulářů + nějaké odkazy, které něco provádějí, tak každý má jedinečný token. A ty tokeny se mění po aktualizaci stránky, takže mě pak ani nevadí, kolik má otevřených tabů.

ikona Jakub Vrána OpenID:

Ano, toto řešení je popsané na http://php.vrana.cz/cross-site-request-forgery.php. Zde jsem použil nejjednodušší obranu proti CSRF, aby vynikla myšlenka automatické obrany.

ikona David Grudl:

Vždycky mě na tomto využití ob_start() (třeba i v dokumentaci) zaráželo, kde programátoři berou jistotu, že se neodešle '...<form me' v jednom bufferu a 'thod="post">...' ve druhém. Kde ji bereš ty? ;)

ikona Jakub Vrána OpenID:

Ještě nedávno bylo v dokumentaci napsáno, že se výstup posílá až s koncem řádku: http://cvs.php.net/viewvc.cgi/phpdoc/en/reference/…&view=markup. Pak jsem to ale na základě bugu http://bugs.php.net/bug.php?id=42660 vyhodil.

Pořád se na to ale podle mě většinou dá spolehnout, protože funkce se volá po chuncích a chunk je buď jedno echo nebo jeden blok HTML kódu. Takže pokud nevypíšeš půlku značky v jednom echu a druhou půlku v druhém (nebo ve dvou blocích HTML kódu), tak to bude fungovat. To se na druhou stranu může stát asi docela snadno:

<form action="<?php htmlspecialchars($_SERVER["REQUEST_URI"]); ?>" method="post">

Takže by kód měl udržovat informaci o stavu.

ikona David Grudl:

Teď jsem to zkoušel v PHP 5.2.8 a chunk se rozdělí i uvnitř HTML kódu, nicméně výchozí hodnota 0 nerozděluje vůbec (pokud se manuálně nezavolá ob_flush()).

Takže odpovídám si sám, mělo by to fungovat. Pochybnosti jsem měl z toho důvodu, že kdysi to fungovalo trošku jinak. Snad se dá na hodnotu "0" spolehnout.

ikona Jakub Vrána OpenID:

Dej sem prosím ukázkový kód. Já jsem to taky zkoušel a nepodařilo se mi to reprodukovat.

Nula pokud vím fungovala vždy a dokument zpracovala až na konci. Problém s nulou je ten, že se celý dokument pošle až když je zpracován na serveru. Takže uživatel čeká a čeká.

ikona David Grudl:

<?php

$counter
= array();

function
callback($buffer)
{
      $GLOBALS['counter'][] = strlen($buffer);
      return $buffer;
}

ob_start("callback", 123); // realne bufferuje po násobcích 400
?>******* tady nasleduju par kB znaku********<?php

print_r
($counter);
?>

ikona Jakub Vrána OpenID:

Díky za příklad, konečně se mi to podařilo zreprodukovat. Zkoušel jsem to vždycky na krátkých textech a teprve "pár kB znaků" mělo kýžený následek.

Opravil jsem to tak, že jsem funkci ob_start() sebral druhý argument, ale dalo by se to vyřešit také přidáním následujícího kódu na začátek csrf_token():
<?php
   
static $rest = "";
    $s = $rest . $s;
    list($s, $rest) = preg_split('~(<[^>]*$)~', $s, -1, PREG_SPLIT_DELIM_CAPTURE);
?>
Nemám to ale pořádně vyzkoušené.

ikona david@grudl.com:

Asi tak nějak. Plus sledovat druhý parametr callbacku.

ikona PaBi3:

Za zmienku stojí aj použitie funkcie output_add_rewrite_var().

pojízdná kočka:

@Jakub: ten <form action="<?php htmlspecialchars($_SERVER["REQUEST_URI"]); ?>" method="post"> pravděpodobně neudělá to, co jsi zřejmě zamýšlel (pokud jsi zamýšlel, že má něco vypsat).

kontrolní otázka: proměnná $hidden je stejná pro celé zpracování, správně? nebylo by lepší, kdyby se nastavila jednou (např. hned po vygenerování tokenu) a pak použila? Na druhou stranu uznávám, že v kombinaci s output bufferingem, který se použije jednou, by to tak mohlo zůstat.

v regulárním výrazu: ..je jednoduchý apostrof v řetězci uzavřeném v jednoduchých apostrofech. nestopne se to v půlce řetězce? a taktéž.. ať se na něj koukám jak se koukám, podle mě se $hidden vloží právě v případě kdy action začíná na "http". mýlím se (kde)?

článek je to pěkný, ale vytvořit si něco jako $hidden v tomto příkladu a vložit ho do každého formuláře s method="post" zase není tak strašné, imho...

ikona Jakub Vrána OpenID:

Apostrof je escapovaný, nic se tedy nestopne.

V regulárním výrazu jsi přehlédl (?! - negativní dopřednou aserci.

Jakub Suchy:

Ach jo, kdy uz lidi pochopi, ze htmlspecialchars($promenna) NEJSOU Ochranou proti XSS? Jakube, to je fakt zaklad.

ikona Jakub Vrána OpenID:

Můžeš to víc rozebrat?

Jakub Suchy:

htmlspecialchars() nezachyti string v UTF-7. Napr. starsi IE je tomu nachylny, pote string standardne interpretuje a XSS je na svete. Referencni spravna implementace: http://api.drupal.org/api/function/check_plain/6

ikona Jakub Vrána OpenID:

Ty pořád s tím UTF-7 – na webu se tohle kódování prostě nepoužívá. Tebou uvedený odkaz navíc řeší jiný problém – funkce htmlspecialchars() ošetří všechny výskyty speciálních znaků i v nevalidních UTF-8 řetězcích.

Tomas:

A co kdyz ti nekdo proste string v utf7 predhodi? To nic nemeni na tom, ze utf7 nepouzovas

ikona Jakub Vrána OpenID:

Když se stránka posílá s kódováním UTF-8, tak se v UTF-8 i interpretuje. UTF-7 řetězec poslaný v UTF-8 je zcela neškodný.

Aby se stránka interpretovala v kódování UTF-7, tak bych ji v tomto kódování musel i dobrovolně odeslat.

Martin Straka:

Jakube, mně se to líbí. Jenom (uznávám je to trochu hnidopišské a teoeretické:) to přidává token i do formulářů, které vedou případně jinam než zpět na vlastní web.

ikona Jakub Vrána OpenID:

To je naopak výborná připomínka. Navíc vzhledem k tomu, že je token jenom jeden, tak by jeho odeslání na cizí server znamenalo pro uživatele katastrofu. Jednoduše mě nenapadlo, že by na stránce mohly být formuláře odesílané na cizí server.

Kontrolu před posíláním na cizí server jsem doplnil. Pokud action začíná na http, tak se token nedoplní. To samozřejmě znamená, že se nedoplní ani u odkazů na vlastní server zapsaných v absolutní URL.

ikona David Grudl:

K dokonalosti zbývá zahrnout i URL action="//php.vrana.cz/send.php"

ikona Jakub Vrána OpenID:

My víme, ale to už je opravdu jen teorie :-).

ikona Jakub Vrána OpenID:

Doplnil jsem.

ikona ehmo:

Takato automaticka kontrola je fajn, ale nastava tam niekolko problemovych situacii, ako uz bolo napisane, napriklad odoslanie na formularu na iny server.

Celkom ma ale zaujima, preco sa musi jednat len o formular odosielajuci data metodou POST, pricom CSRF je aplikovatelne prakticky na akukolvek metodu, a hlavne GET. Taktiez generovanie tokenov by malo byt jedinecne pre kazdy formular, resp. kazdu interakciu osobitne. Comu nerozumiem dalej je dovod, preco by ochrana proti CSRF nemohla byt uspesne v pripade autentifikacie uzivatela za pomoci cookies. Napriek tomu, ze nevyziadana interakcia uzivatela bude prebiehat pod jeho prihlasenim, vykonana nebude, pretoze token nebude sediet. Alebo som nieco prehliadol?

ikona Jakub Vrána OpenID:

Odeslání formuláře na jiný server už je ošetřené. Pokud víš o dalších problémech, tak sem s nimi.

CSRF slouží k provedení operací. Operace by webová aplikace měla provádět zásadně metodou POST. Proto stačí ošetřit jenom formuláře odesílané touto metodou (ošetřovat např. vyhledávací formulář je zbytečné).

Generovat více tokenů má smysl v případě, že hrozí jejich vyzrazení (pak je lze zneužít jen pro danou operaci, pokud ještě nebyla provedena). Psal jsem o to v článku http://php.vrana.cz/cross-site-request-forgery.php, zde jsem z důvodu názornosti použil nejjednodušší bezpečnou variantu.

Pro identifikaci uživatele se v PHP používají session proměnné. Aplikace je samozřejmě může emulovat (nebo použít HTTP autentizaci) – pak je potřeba stejný postup použít i v obraně proti CSRF. V článku je to dostatečně rozebrané.

ikona ehmo:

http://web.com/edit?telefon=cislo mas pocit ze toto nie je operacia? Asi si si CSRF striktne vzal len ako zranitelnost pre editaciu, ukladanie a pod. CSRF moze byt taktiez vyborne pouzite ako urcite forma DoS (no povedzme aj DDoS, ale to je uz o niecom inom). Ak mas vyhladavanie necachovane a prehladavas privelku tabulku, nie je nic lepsie, ako urobit <iframe src="http://domena.com/hladaj?q=a"></iframe> nic viac nepotesi ako taketo neustale prechadzanie pomaly celej db (samozrejme zalezi od webu, velkosti a optimalizacie db, atd.) CSRF je rovnako dobre pouzitelne pre rozne AJAX(J) callbacky, a prakticky vsade tam, kde sa da vykonat akakolvek interakcia.

generovanie unikatnych tokenov pre kazdy formular je aj jedna z foriem, ako ztazit robotu utocnikovi v pripade, ze objavi XSS, ktora prakticky uplne potlaca ochranu CSRF.

neviem z akeho dovodu sa v php pouzivaju pre identifikaciu uzivatela zrovna session premenne, ja sa bez problemov stretavam s autentifikaciou cez cookies ktora funguje rovnako dobre. uz aj z toho dovodu, ze php nie je jediny jazyk v ktorom sa pisu weby a csrf postihuje akykolvek web napisany v hocijakom jazyku. napriklad google a mnoho inych pouziva cookies pre autentifikaciu ako aj identifikaciu uzivatela bez vacsich problemov.

ikona Jakub Vrána OpenID:

Pro DoS se dá využít libovolná stránka, ani nepotřebuješ odesílat žádné formuláře. Třeba sestavení titulní stránky může stát víc práce než vyhledávání, takže používat token jako ochranu před DoS je holý nesmysl.

Pokud AJAX callback provádí nějakou operací (a neslouží jenom k získání dat), tak by samozřejmě měl být také posílaný POSTem. Když se AJAXový požadavek sestaví z formuláře jako to je popsané na http://php.vrana.cz/odeslani-formulare-pres-ajax.php, tak na něj bude automatická obrana fungovat. Pokud se sestaví ručně, tak je potřeba token doplnit (kontrola zůstává automatická).

Pokud je server zranitelný pomocí XSS, tak se o CSRF nemá smysl vůbec bavit, protože nepomůže žádná obrana.

Samozřejmě jsou i jiné způsoby identifikace uživatele, článek na to upozorňuje. Toto je web o PHP a v PHP se nejčastěji používají session, automatická obrana tedy využívá právě je.

Martin Straka:

No, když je XSS tak klasická tokenová ochrana proti CSRF je samozřejmě nahraná.

Ale mohou existovat i jiné věci, i taková captcha se dá svým způsobem chápat jako obrana proti CSRF, nebo třeba kód poslaný SMS uživateli a jeho ověření, nebo třeba poslání doporučeného dopisu s notářsky ověřeným podpisem:)

ikona v6ak:

Co se týče SMS, doporučených dopisů apod., jde se na to dívat ze dvou pohledů:

Z pohledu uživatele jde skutečně o CSRF ochranu - akce se skutečně neprovede. Ale otravuje to.

Z pohledu provozovatele to už je horší. Nevyžádaná akce stojí náklady, přestože nic nepřináší.

Provozovatel se může rozhodnout tyto akce regulovat nebo zpoplatnit. (Spíš to první.) Pak se to stává nepochybně CSRF i pro uživatele, protože odebrání z limitu je taky akce, ne? DoS může být někdy velmi nepříjemný a pro provozovatele až smrtící, pokud se dtkne většího množství uživatelů a ti to "zpopularizují".

Jiří Málek:

Já budu reagovat na ehmo: a není možné, že qooqle si identifikuje uživatele pomocí cookie proto, protože ke každému uživateli ukládá +- 20 cookie, což při vynásobení počtu uživatelů nemusel server stíhat? Protože Sessions zůstanou jak na straně klienta, tak i na straně serveru?

Robert Vlach:

Jakube díky za skvělé články o CSRF! :-) Zajímala by mne jedna věc: existuje nějaká možnost, že by se útočník k tokenu přece jen dostal? Představuji si to např. tak, že by uživateli podstrčil stránku A, která by skrytě obsahovala také stránku B s vygenerovaným tokenem, a následně by po provedení nějaké akce na stránce A mohlo dojít také ke spuštění akce na stránce B (nevím jestli se to dá přes JavaScript takto vynutit?). Doufám, že jsem to popsal srozumitelně :-)

ikona Jakub Vrána OpenID:

Token může uniknout, pokud je aplikace zranitelná pomocí XSS. Proti ostatním případům nás chrání Same origin policy prohlížečů.

Lamicz:

Zdravím, díky za super nápad, už jsem to implementoval do svého systému, omlouvám se za "lama" dotaz, proč je ve fci csrf_token proměnná $s a co dělá (na reg. výrazy jsem max lama...). Dík ;)

ikona Jakub Vrána OpenID:

$s je obsah odesílaného dokumentu, naplňuje ho funkce ob_start(). Regulární výraz doplní token ke všem formulářům odesílaným metodou POST s relativním URL.

ikona PeTaX:

Tak nevím, ale mám pocit, že když po kontrole tokenu pošleš headrem na jinou stránku, tak ten POST už tam bude prázdný.
Trošku mi uniká, že nikde není výstup ob_end. Že funkci spustí je jasné, ale kde se výstup té fce vezme? Odkud přijde $s? Sry, jsem už na stará kolena natvrdlý.

ikona Jakub Vrána OpenID:

Ano, po uložení dat se přesměruje na stránku s informací o výsledku operace. Tam jsou POST data samozřejmě nežádoucí.

Funkce se automaticky zavolá na konci skriptu.

Kočičác Bonifák:

Můj skromný názor, je ten, že pokud si skrytý token uložím na začátku do proměnné a tu pak vložím do formuláře, tak to bude řešení, které má menší potenciál ve stránkách vyvolat nějakou chybu nebo problém obecnějšího rázu (absence podpory nějaké funkce v dané distribuci, atp.) Troufám si tvrdit, že nejvíce stránek obsahuje 0 formulářů metody POST, menšina jeden či dva a pouze malé procento víc než dva. To, že generování tokenů "je nudná práce", bych nějak dále nekomentoval, jen si myslím, že není nudnější než cokoli ostatního. S tím, že na ni může člověk zapomenout, souhlasím, a kvituji, že při vývoji se v ideálním případě nemělo zapomínat na nic. Ve srovnáním s výše uvedeným řešením, je navíc tak desetkrát větší pravděpodobnost, že člověk zapomene na něco během konstrukce Jakubova řešení, popřípadě při tom se setká s jinými problémy jiného rázu; a stráví nad nimi desetkrát více času; zvýší pravděpodobnost zavlečení syntaktických či jiných chyb; "nakyne" si svůj kód, čímž učiní o trochu obtížnější v něm něco hledat; zvýší požadavky na nasazení takové aplikace (zajisté existují distribuce, které output buffering nemají) a tím sníží přenositelnost... mohl bych pokračovat...

ikona Jakub Vrána OpenID:

FUD. Output buffering je pevnou součástí PHP a neexistuje distribuce, která by ho nepodporovala. Využívají ho i interní funkce PHP jako třeba print_r() s druhým parametrem.

Ta tvoje „desetkrát vyšší pravděpodobnost“: S klasickým řešením můžeš zapomenout vložit token nebo ho zkontrolovat, s automatickým řešením ho můžeš jen zapomenout zkontrolovat. Vychází mi to tedy na polovinu. Navíc by šlo řešení udělat tak, aby bylo zcela automatické, to by ale zhoršilo chování aplikace (např. by se nepředvyplnily hodnoty ve formulářových polích).

Kočičác Bonifák:

Dobrá,
uznávám, že v distribuci bez output bufferingu jsem se mýlil. Co se týče zbytku, o žádný FUD se nejedná (myslím, že tu zkratku používáš ve špatném kontextu, protože já nemám žádný zájem pomlouvat ob a PHPčko, ve kterém nota bene rád pracuju).
Dodal bych ale, že oproti běžnému řešení, které vše hned vysílá na výstup, output buffering zabírá paměť a ty nevíš, jak bude ta část, co se do něj ukládá, velká. Některá nastavení počítají např. jen s 8 MB paměti pro skript, některá i méně. Může být kód ve formuláři větší? Může! V takovém případě klasické řešení poběží vesele dál, kdežto to tvoje hodí chybovou hlášku a uživatel se dál nedostane. Též si nejsem jistý, jak tvůj skript zachová, pokud HTML formuláře bude součástí stránky jako příklad (v <pre>, <textarea> apod.). Furt mi to přijde jako takové 'látováné' řešení, rozhodně ne elegantní.

ikona Jakub Vrána OpenID:

Poznámka FUD se vztahovala pouze k argumentaci distribucemi bez OB. Paměť zabraná OB může být minimální, vůbec tam nemusí být celá stránka - http://php.vrana.cz/automaticka-obrana-proti-csrf.php#d-7572

Uvnitř značky <textarea> být značky nesmí (jsou nevalidní), uvnitř <pre> se normálně interpretují (<form> je opět nevalidní).

Kočičác Bonifák:

Zdravím. Vím, k čemu se tvá poznámka vztahovala, a to, co jsem řekl o tvém možném nepochopení významu toho pojmu resp. kdy se dá použít, zůstává.

Ano, to, co říkáš o < a > uvnitř některých značek, je všechno pravda, ale neznamená to a ani z toho automaticky nevyplývá, že je člověk přesto nemůže použít. Pak se u tvého řešení v obou případech vkládaný kód změní, aniž by to uživatel tušil.
Imho, napadlo mě ještě pár příkladů, např.,

<form action=url-stranky ...>

<form onclick='action="abc";' action="url" ...>

<form onclick='promenna=">";' action="url" ...>

u kterých (si myslím) že tvůj regulární výraz selže (oprav mě, jestli se mýlím).
pak např. v HTML poznámce (tam to zřejmě moc vadit nebude)
uvnitř JavaSkriptového kódu (např. opět, když si začátek toho kódu formuláře budu z nějakého důvodu ukládat do proměnné a pak s ní dál operovat) může natropit paseku a způsobit nefunkčnost celé stránky, což tedy alespoň mě přijde jako problém.
Myslím, že by šlo najít ještě pár příkladů, jak může tvoje metoda (jakkoli myšlena v dobrém úmyslu) být kontraproduktivní. (Co když daný formulář například v JavaScriptu přistupuje k jeho vstupním prvkům přes index? vložením tvého CSRF pole se indexy změní. A tak dále.)
Těch nevýhod není zase tak málo, jak by se mohlo zdát. Jak si uvedl ty, alternativa manuálního vkládání má nevýhodu v tom, že CSRF kontrolu ve formuláři mohu zapomenout přidat. (Napadají tě ještě nějaké?)

Shrnul bych to do toho, že v obou případech (u manuálně i automaticky vkládaných formulářů) si musí programátor dát na pár věcí pozor.

Podle tvých slov vychází "na polovinu". Ve světle toho, co jsem napsal výše, si to já osobně nemyslím.

ikona Jakub Vrána OpenID:

Máš pravdu. Aby ochrana fungovala ve zcela obecném případě, tak by se musel provést mnohem pečlivěji. Takhle pokryje jen zhruba 99% všech případů.

Kočičác Bonifák:

Vážím si toho, že jsi to uznal (i když tvá odpověď možná má(?) lehkou příchuť sarkasmu).
Představ si nějaký redakční systém s tvojí CSRF obranou, píšící o HTML (a formulářích jako část z toho), který, dejme tomu, tuto kontrolu validity nemá. Půjde v tomto případě pouze o 1 % selhání?
Nebo si představ článek, který bude obsahovat fungující příklad na formulář metody POST, který si k němu pomocí JavaScriptu přistoupí, nechá si ho vypsat a následně bude popisovat, co je 1. prvek v tomto formuláři, co je 2., 3., ...atd. Tvoje obrana proti CSRF do formuláře vloží skrytý prvek na 1. místo a tím pořadí rozhodí (aniž by autor onoho článku cokoli tušil). Nenapadá mě způsob, jak něco takového 100% podchytit (snad kromě naprogramování umělé inteligence).

Ad tvých 99 procent. Věz, že 99% jistota je nejistota. Navíc, pokud tvoje řešení selže, může u daného řešení trvat dlouhé hodiny, které lidé promrhají na to, aby na chybu přišli.
Ad pečlivější ochrana: troufám si tvrdit, že by to nešlo dotáhnout na 100% jakýmkoli regulárním výrazem (pokud je to to, co jsi měl na mysli). Možná by to šlo kontrolou kontextu, pokud by byla opravdu pořádná. S tím ale roste složitost - u malých projektů to pak bude vypadat jak "kanónem na vrabce" a opět hrozí, že se na to začne synergicky nabalovat přehršel problémů, které jdou ruku v ruce (složitost a objemnost kódu a klesající "vyznatelnost" se v něm, riziko zatažení evidentních i latentních sémantických chyb, podle použitých funkcí možné problémy s kompatibilitou a znovupoužitelností, neochotu něco takového upravovat, ...).
Toto jsou nevýhody tvého řešení. Plus potenciální problém s ob_flush(), plus možný problém s pamětí (ještě mě napadá jistý spíše teoretický problém s implementační závislostí, ale ten se dá do jisté míry upravit u obou řešení).
Jestli to (podle podnázvu tvého blogu) hodnotíš jako 'elegantní', nechávám na tvých čtenářích.

Běžné řešení naproti tomu JE stoprocentní, nikam nepřidává žádný kód bez vědomí uživatelů, a jde v něm jen o to vložit (individuálně nebo pokud si na to napíšeš funkci tak pomocí ní) skryté pole do každého formuláře, který něco přepisuje nebo maže. Na to programátor zapomenout může, ale aby mu to uniklo, musel by i za stejných záhadných okolností zapomenout i na kontrolu CSRF při zpracování.

P.S: Neber, prosím, toto všechno jako kdovíjak zdrcující kritiku. Máš u mě respekt, že jsi na tento přístup vůbec přišel a že jsi ho zveřejnil.

ikona v6ak:

Já tento kód beru spíše jako myšlenku "vždyť to nemusí být tak těžké". Mnohem více se mi ale líbí řešení (nejen myšlenka) v Nette.

kesspess:

A co tohle (jakožto obsah útočného skriptu):

<?php
session_start
();
$_SESSION['csrf_token'] = 'ahoj';
?>

<form action="zranitelna-stranka.php" method="post">
<input type="hidden" name="csrf_token" value="ahoj">
...
</form>

ikona Jakub Vrána OpenID:

Session proměnná se nastaví na stránce útočníka, to aplikaci nijak neublíží.

Martin:

Zpomaluje ob_start funkce nějak běh aplikace?

Myslel jsem si, že PHP odesílá data okamžitě při čtení scriptu, takže jsem se asi mýlil. Znamená to, že ho projde celý a na konci použije ob_start a pak projde jej znovu, aby připsala do formulářů input s tokenem?

ikona Jakub Vrána OpenID:

PHP skript nejdřív parsuje. Teprve zparsovaný skript dále zpracovává. Při použití ob_start k drobnému zpomalení asi dojde, ale neřekl bych, že k výraznému. Nejlepší je si to změřit na vlastní aplikaci.

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.