PHP triky

Weblog o elegantním programování v PHP pro mírně pokročilé

Adminer 4.1.0

Asi největší novinkou právě vydané verze Admineru je ochrana proti pokusu o získání hesla k databázi hrubou silou. Adminer nově dovolí jen 30 neplatných pokusů o přihlášení za 30 minut (počítáno od prvního neplatného pokusu). Další pokusy (ať už se správným nebo špatným heslem) se ignorují, dokud nevyprší časový limit. Limit se vztahuje na IP adresu, nikoliv na uživatelské jméno. To je ze dvou důvodů:

  1. Útočník může zkoušet různá uživatelská jména, nemusí být zaměřen jen na jedno.
  2. Při vazbě na uživatelské jméno by útočník mohl oprávněnému uživateli zabránit v přihlášení úmyslným zadáváním špatných hesel (DoS).

Limit je poměrně velkorysý, protože mě rozčiluje, když mi některé služby třeba po třech neplatných pokusech o přihlášení ukážou dlouhý nos. Také jsem zohlednil, že stejnou IP adresu může používat více uživatelů, např. ze stejné firmy s proxy serverem. Pokud máte webový server umístěný za reverzní proxy a všechny požadavky chodí ze stejné IP adresy, tak je vhodné implementovat metodu Adminer::bruteForceKey() tak, aby vracela poslední část hlavičky X-Forwarded-For. Jinak vám hrozí DoS. Hlavička X-Forwarded-For se bohužel nemůže kontrolovat automaticky, protože jde triviálně podstrčit.

Přemýšlel jsem i o přidání CAPTCHA, což by zamezilo riziku DoS. reCAPTCHA ale používá privátní klíč, který by musel být součástí Admineru, a ostatní řešení jsou obvykle celkem snadno prolomitelná.

Data se ukládají do upload_tmp_dir nebo pokud není nastaven, tak do sys_get_temp_dir(). Tyto adresáře někdo pravidelně promazává, nicméně vzhledem k poměrně krátké platnosti dat by to nemělo moc vadit.

I přes tuto ochranu je vhodné přístup k Admineru omezit, např. v Apache pomocí Access Control. Případně pokud Adminer použijete někde jen jednorázově, tak ho zase po sobě smažte. Odkaz na Adminer také nikam zbytečně nedávajte a neumisťujte ho na snadno uhodnutelnou adresu. Pokud Adminer používají zákazníci vašeho hostingu, tak jim k němu umožněte přístup jen pokud jsou přihlášeni do administrace hostingu.

Další novinky

Jakub Vrána, Adminer, 18.4.2014, diskuse: 9 (nové: 9)

Přechod na SafeHtml

Ve většině moderních aplikací vzniká HTML kód převážně v šablonách. To má nespornou výhodu v tom, že ošetřování dat stačí vyřešit na jednom místě. Můžeme použít automatické escapování, kontextově citlivé escapování, můžeme úplně zakázat vypsat text bez jeho ošetření. I v těchto aplikacích může být ale šikovné si tu a tam vygenerovat HTML kód, který do šablony pošleme. Můžeme totiž využít plnou sílu programovacího jazyka – metoda může vracet HTML kód a potomek ji může přetížit a vrátit něco jiného. Samozřejmě si tyto metody vevnitř můžou také zavolat šablonu, ale jednak někdy šablonovací systém nemají po ruce a jednak to je trochu jako kanón na vrabce, když chtějí vrátit třeba jenom <em>text</em>.

Facebook

Facebook žádné šablony nepoužívá a když nějaká část programu chce vytvořit HTML, tak to udělá pomocí XHP. Vede to k trochu odlišnému způsobu programování – místo velké aplikace, která generuje datové struktury, a velkých šablon, které tyto struktury zpracovávají a které jsou od hlavního kódu obvykle poměrně daleko, je všechno pěkně pohromadě. XHP je obvykle malé a jednoduché, protože vytváří jen malou část výsledku s využitím dalších komponent, které taky vytváří XHP. Všechny výhody šablon týkající se ošetřování dat jsou přitom díky XHP zachovány.

Phabricator

Phabricator vznikl ve Facebooku a je napsán do značné míry podobně. Nepoužívá žádné šablony, HTML kód se vytváří lokálně s využitím síly programovacího jazyka. Problém je, že nemá k dispozici XHP, takže HTML kód se vracel jako ručně ošetřované řetězce. Vzhledem k tomu, že kód vytváří velmi zkušení programátoři a všechny změny prochází code review, tak v historii nedošlo k mnoha případům nedostatečného nebo nadbytečného ošetřování. S kódem se ale pracovalo poměrně nepohodlně především proto, že nebylo jasné, co vrací nebo přijímá správně ošetřený HTML kód a co vrací nebo přijímá čistý text, který je nutno ošetřit.

Navrhl a realizoval jsem proto změnu, po které se všechny řetězce považují za čistý text a pokud něco chce vytvořit ošetřený HTML kód, tak musí vrátit PhutilSafeHtml. Vtip je v tom, že tento objekt si nemůže vytvořit jen tak někdo, ale musí k tomu použít jednu ze dvou funkcí. Buď phutil_tag, která přijímá název značky, pole atributů a obsah, nebo hsprintf, která přijímá formátovací řetězec a libovolný počet hodnot, které se před dosazením ošetří. Obsah může být buď řetězec, který se automaticky ošetří, nebo PhutilSafeHtml, kterému se věří. hsprintf v prvním parametru přijímá jen konstantní řetězec, za což ručí lint rule.

Při pohledu zpět se funkce hsprintf ukázala jako zbytečná – někdy je sice jednodušší ji použít, ale zbytečně zmírňuje silné záruky, které dává funkce phutil_tag. Ta dovoluje vytvořit jen správně spárované HTML značky, navíc může kontrolovat např. i hodnotu atributu <a href="">, aby nezačínala třeba javascript:. hsprintf nic z toho dost dobře dělat nemůže. Úplně by stačila funkce vytvářející značku a funkce spojující kombinaci neošetřených řetězců a PhutilSafeHtml do jednoho PhutilSafeHtml. A možná ani ta ne, protože funkce jako obsah přijímá i pole hodnot, které zřetězí.

Když odhlédnu od množství nudné práce, kdy bylo potřeba všechny řetězce vytvářející HTML kód převést na jednu z těchto dvou funkcí, tak byla změna vlastně docela jednoduchá. Pokud někdo konzumoval čistý text a dostal PhutilSafeHtml, tak se zavolala jeho metoda __toString. Maximálně hrozilo, že pokud tento text poslal dál, tak se při příštím použití znovu ošetřil. Tyto případy se nám podařilo celkem rychle vychytat. Pokud někdo začal konzumovat PhutilSafeHtml a dostal řetězec, tak ho prostě poslal dál – všechny funkce považují řetězec za platný vstup, který se na PhutilSafeHtml ve finále převede jeho ošetřením. Díky tomu není potřeba nikde v kódu ručně ošetřovat jakákoliv data.

Proč mi změna přišla vlastně docela jednoduchá, i když se týkala desítek tisíc řádek kódu? Hlavní důvod je ten, že celý kód máme pod kontrolou – pokud se rozhodneme změnit API nějaké metody, tak můžeme změnit i všechna její volání.

Closure Library

Obdobnou změnu nyní pomáhám realizovat i pro Closure Library. Closure Tools sice obsahuje vynikající šablony Closure Templates (alias Soy), knihovna na nich ale nezávisí, takže je nemůže používat.

Snažíme se odstranit všechna místa, která přiřazují řetězec do innerHTML, a nahradit je voláním funkce, která přijímá SafeHtml. Pokud budete chtít SafeHtml vytvořit z uživatelského vstupu, tak k tomu můžete použít funkci SafeHtml.from, která pro vás data ošetří. Pro vytvoření něčeho složitějšího jsem původně navrhl takovéto API:

var html = new SafeHtmlBuilder(TagName.A)
	.setAttribute('href', href)
	.addContent(text)
	.addContent(new SafeHtmlBuilder(TagName.BR).build())
	.build();

Byl to moc velký Javismus, takže jsme nakonec skončili u něčeho podobného jako ve Phabricatoru: SafeHtml.create('a', {'href': href}, [text, SafeHtml.create('br')]). Kromě toho vznikla ještě funkce SafeHtml.concat umožňující spojit více SafeHtml do jednoho.

Převod stávajícího API tak jednoduchý jako ve Phabricatoru bohužel nebude. Vezměme si třeba metodu goog.ui.tree.BaseNode#getHtml. Ta může být jak přetížená, tak ji může kdokoliv volat. Nemůžeme ji tedy změnit tak, aby prostě začala vracet SafeHtml, protože bychom všechny rozbili a navíc by naši potomci SafeHtml stejně nevraceli. Zvolili jsme tedy takovéto, poněkud krkolomné řešení:

Závěr

Zápis HTML jako řetězce sice napomohl jeho masivnímu rozšíření, ale ještě dnes přináší problémy i ve velkých společnostech. Pokud někde ve svém kódu uvidíte něco jako return "<br/>", tak je nejspíš vaše infrastruktura špatně a vyplatí se investovat energii do toho ji co nejdříve změnit. Čím později to uděláte, tím to bude bolestivější.

Jakub Vrána, Řešení problému, 12.3.2014, diskuse: 5 (nové: 5)

Adminer 4.0.0

Hlavní novinkou právě vydané verze Admineru jsou ovladače pro NoSQL databáze. Konkrétně SimpleDB, MongoDB a Elasticsearch. Poslední dva jsou nedodělané a jsou označené jako beta, i tak jsem se je ale rozhodl zveřejnit – hlavně pro případ, že by je někdo chtěl dokončit. Sám MongoDB a Elasticsearch momentálně nepoužívám, tak k tomu mám malou motivaci.

Dalších novinek je také požehnaně:

Opravil jsem také několik chyb zavlečených v předchozích verzích:

Několik vylepšení se týká také bezpečnosti:

Některé novinky jsou k dispozici jen v některých ovladačích:

Přibyla portugalština používaná v Portugalsku a thajština.

Jakub Vrána, Adminer, 8.1.2014, diskuse: 82 (nové: 82)

Ukládání draftu komentářů

Taky vás dokáže naštvat, když delší dobu píšete komentář, dáváte si záležet na každé formulaci a pak se něco stane a o celý komentář přijdete? To něco může být chyba serveru, vaše nepozornost při zavírání oken nebo třeba agresivní WiFi, která jakýkoliv pokus o nahrání stránky přesměruje na svůj přihlašovací formulář.

Jako tvůrce aplikace tomu můžete zabránit ukládáním pracovní verze komentáře do nějakého úložiště. Komunikace se serverem po napsání každého znaku by byla příliš drahá, cookies nemají dostatečnou kapacitu a práce s nimi je nepohodlná, ale dá se použít úložiště localStorage. Je dostatečně velké (obvykle 5 MB na doménu) a ukládá se na klientovi, takže práce s ním je rychlá a nezabírá místo na serveru.

Kód je celkem přímočarý:

<textarea id="comment" name="comment" rows="20" cols="80"></textarea>
<script type="text/javascript">
if (window.localStorage) {
	var comment = document.getElementById('comment');
	var key = 'comment:' + location.pathname + location.search;
	comment.onkeyup = function () {
		localStorage.setItem(key, this.value);
	};
	if (!comment.value) {
		comment.value = localStorage.getItem(key);
	}
}
</script>

Kód funguje správně, pokud je na stránce jen jeden komentář. Pokud jich máme více, tak každému musíme přiřadit unikátní identifikátor, což ostatně můžeme udělat tak jako tak. URL stránky jsem zvolil jen v zájmu o obecnost a často to nemusí být nejlepší adept.

Ke kódu mám pár poznámek:

  1. Pokud uživatel vloží data ze schránky pomocí myši, tak se text neuloží, protože nedojde ke stisku klávesy. Pokud vám to vadí, můžete implementovat událost onpaste nebo data ukládat v pravidelném intervalu.
  2. Když si uživatel otevře tu stejnou stránku vícekrát, načte se mu do oken stejný obsah. Také se zachrání jen poslední text, který upravil. Nenapadá mě moc, jak by se to dalo řešit.
  3. Před nastavením komentáře se kontroluje, jestli je prázdný. To je proto, že některé prohlížeče za určitých ukolností zachovají obsah formulářů i po obnovení stránky (např. Firefox, pokud je povoleno ukládání stránky do keše). To řeší alespoň část předchozího bodu.

Zbývá smazání draftu po uložení finální verze. Mohlo by svádět to udělat v onsubmit, to by ale nepokrylo několik problematických scénářů. Skript, který provádí zápis do databáze, obvykle nic nevypisuje a jen provede přesměrování. Tam to tedy taky udělat nemůžeme. Můžeme si ale nastavit session proměnnou, kterou zkontrolujeme na stránce, kam uživatele přesměrujeme:

<?php
if (isset($_SESSION["comment_saved"])) {
	unset($_SESSION["comment_saved"]);
	?>
	<script type="text/javascript">
	if (window.localStorage) {
		var key = 'comment:' + location.pathname + location.search;
		localStorage.removeItem(key);
	}
	</script>
<?php } ?>

Klíč by bylo vhodné inicializovat na centrálním místě, v ukázce jsem to rozdělil jen kvůli pochopitelnosti.

Jakub Vrána, Řešení problému, 18.12.2013, diskuse: 34 (nové: 34)

Ukládání hesel bezpečně

Článek vyšel v Crypto-Worldu 11-12/2013.

V Crypto-Worldu 9-10/2013 vyšel článek „Ochrání hashování uživatelská hesla?“, který na otázku z titulku bohužel neodpověděl. V tomto článku bych tedy rád popsal, jak uživatelská hesla správně ukládat.

V první řadě musíme vyloučit nesmyslná řešení jako ukládání hesel v čistém textu nebo jejich šifrování. Jsou špatná proto, že heslo se nesmí dozvědět ani provozovatel aplikace, který má přístup ke všem algoritmům, datům a klíčům. Pro ukládání hesel je potřeba použít hašování a k heslu přiložit náhodnou sůl. Tu přidáváme především proto, aby nebylo poznat, že dvě hesla jsou stejná, ať už hesla dvou uživatelů u stejné služby nebo hesla stejného uživatele u dvou různých služeb. Druhým důvodem je zamezení použití rainbow tables – předpočítaných tabulek hašů.

Pomalé hašovací funkce

Víme už tedy, že hesla musíme hašovat a že k nim musíme přiložit náhodnou sůl. Zbývá výběr hašovací funkce. Běžné hašovací funkce jako MD5, SHA-1 nebo SHA-2 jsou pro ukládání hesel nevhodné proto, že jsou příliš rychlé. MD5 hesla složeného z osmi malých písmen prolomí oclHashcat na celkem běžné grafické kartě průměrně za 10 sekund (26^8 / 10.742e9 / 2)! Pokud požadavek na heslo zpřísníme třeba tím, že budeme vyžadovat použití i velkých písmen a čísel, tak řada uživatelů dá velké písmeno na začátek a číslo přidá na konec. Takto zkonstruované heslo útočník prolomí dokonce za 4 sekundy. A to vůbec nemluvíme o použití slovníků nebo hesel získaných z dříve uniknutých databází.

Týrat uživatele požadavky na dlouhá a složitá hesla zkrátka není řešení. Geekům to možná vadit nebude, protože si heslo uloží do klíčenky, ale pro běžné uživatele to bude pohroma končící zapsáním hesla na žlutý papírek přilepený na monitor. Řešením je použít hašovací funkci, která je záměrně pomalá.

Pomalé hašovací funkce vám nabídnu tři: Bcrypt, PBKDF2 a Scrypt. Všechny tři se pro ukládání hesel dají použít, všechny mají parametr umožňující nastavit, kolik iterací hašování se má provést. Ten je vhodné nastavit tak, aby se jedno zadané heslo ověřilo na vašich serverech v rozumném čase, např. do půl sekundy. Vaše uživatele to nijak výrazně neomezí a útočníka to podstatně zpomalí.

Kterou funkci vybrat? Bcrypt je asi nejdostupnější, např. v PHP ji používá funkce password_hash a již dlouhou dobu i funkce crypt. PBKDF2 je nejstandardnější, používá se třeba ve WPA. Scrypt se od ostatních liší tím, že kromě počtu iterací používá i konfigurovatelné množství paměti, což bude představovat značný problém pro útočníka snažícího se paralelně louskat více hesel najednou. Osobně bych tedy doporučil použít Scrypt.

Mohlo by vás napadnout i prosté opakované použití rychlé hašovací funkce, třeba miliardkrát. Problém s tímto přístupem je v tom, že můžou vznikat cykly, které velkou množinu vstupů namapují na mnohem menší množinu hašů. Přestože jde spíše o teoretický problém, tak se tomuto řešení raději vyhněte.

Co se starými hesly?

Co dělat v případě, že nevyvíjíte novou aplikaci, ale chcete zabezpečit nějakou stávající, kde máte hesla uložená třeba pomocí MD5? Mohou vás napadnout tato řešení:

  1. Nový algoritmus použít pro nově registrované uživatele a těm stávajícím ho změnit při přihlašování (kdy máte k dispozici heslo v čistém textu).
  2. Uživatelům poslat zprávu a požádat je o změnu hesla.
  3. Pokusit se prolomit všechna hesla a přeuložit je.

Všechno to jsou hlouposti. Správné řešení je nový algoritmus aplikovat na původní haš, uložené heslo tedy bude např. výsledkem funkce scrypt(cost, salt, md5(password)). Všechny stávající uživatele převedete jednorázově a při ověřování hesla aplikujete danou posloupnost funkcí. Pokud se v budoucnu rozhodnete použít jiný algoritmus, jednoduše ho do posloupnosti přidáte.

Pokud máte důvodné podezření, že databáze s nebezpečně uloženými hesly už unikla, tak všechna stará hesla zneplatněte a uživatelům pošlete jednorázový odkaz pro jejich nové vytvoření.

Nejde jen o hesla

Hesla nejsou jediné tajemství, které musíme chránit. U webových aplikací je skoro stejně hodnotný i session identifikátor, který se používá pro přihlášené uživatele. Ten se na serveru často válí v čistém textu, aniž by se o něj někdo staral. Přitom když ho útočník získá, tak může jménem uživatele provádět většinu akcí. Věnujte mu proto stejnou pozornost jako heslu samotnému. Jen rychlost jeho výpočtu může být mnohem vyšší, protože se ověřuje mnohem častěji a při jeho úniku to přeci jen není tak velká katastrofa jako při úniku hesel – přinejhorším můžeme všechny uživatele odhlásit.

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

Jakub Vrána, Dobře míněné rady, 16.12.2013, diskuse: 2 (nové: 2)

Starší články naleznete v archivu.

avatar © 2005-2014 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.