WordPress prý používá 27 % webu. Na následujících slajdech bych chtěl naznačit, co bychom ve WordPressu mohli zlepšit z pohledu bezpečnosti,protože když to uděláme, tak se zvýší zabezpečení poměrně hodně webů. Já vím, ne všichni aktualizují, ale o tom někdy jindy. Slajdy obsahují poznámky, které v původní prezentaci nejsou.
V roce 2016 jsem na konferenci Passwords v Las Vegas spustil mini-projekt, seznam firem, které nějak zveřejnily, jak ukládají hesla svých uživatelů. Chci tak veřejně chválit firmy, které to dělají bezpečně a pokud zatím ne, tak aby začaly.
Způsob ukládání hesel zveřejnil i Facebook, v roce 2014 o tom přednášel Alec Muffett. Odkaz na přednášku je samozřejmě uveden i v mém mini-projektu.
$this→iteration_count_log2 + ((PHP_VERSION >= '5') ? 5 : 3; $count = 1 << $count_log2;
Zajímalo mě, jak bezpečně WordPress hesla uživatelů ukládá. Standardně se používá 8192 iterací osolené MD5, a to i na PHP 7. To není zrovna nejlepší a nejbezpečnější způsob ukládání. Vlastnost $this->iteration_count_log2
je nastavena na 8, $count
je tedy ve finále 8k.
password_hash
, password_verify
Bylo by fajn, kdyby WordPress na novějších verzích používal bcrypt. Od PHP 5.5 je dostupný pomocí funkcí password_hash()
a password_verify()
. Další vhodné funkce pro ukládání uživatelských hesel jsou Argon2i, scrypt, PBKDF2. Jestli chcete vědět více, podívejte se na mojí přednášku o ukládání uživatelských hesel.
WordPress funguje i na PHP 5.2.4 a novější funkce tedy nelze používat (samotný bcrypt je použitelný od PHP 5.3.7, ve starších verzích měl problém s hesly s diakritikou). Bohužel prý existují lidé, kteří přechází z novějších verzí PHP na starší (eh, proč?), takže nejde používat bcrypt pouze na novějších verzích a pro ty 10 let staré verze PHP nechat nevhodnou MD5. Naštěstí existují pluginy, které umožní WordPressu používat bcrypt, např. ten od Roots.
O HTTPS toho bylo již napsáno docela dost, ale evidentně to stále nestačí. Bylo by parádní, kdyby WordPress HTTPS vyžadoval. Minimálně pro administraci, jinak se může stát, že se do ní dostane nějaký mizera, když se v kavárně třeba nevědomky připojíte na zákeřnou Wi-Fi a začnete editovat příspěvek. True story, bro.
:-)
Borek napsal na Twitter, že zakladatel WordPressu publikoval článek na Medium. Na Valentýna. A Daniel tomu dal ❤. Ten článek je o podepisování aktualizací, to WordPressu chybí. Operační systémy své aktualizace mají podepsané již dlouho.
Internet je nebezpečné místo a mohlo by se stát, že aktualizace při stahování někdo změní a WordPress začne používat nějaký nebezpečný upravený kód. Pokud by byly aktualizace podepsané, tak WordPress změnu detekuje a aktualizace nenainstaluje. Naštěstí WordPress běžně neaktualizujete přes veřejnou Wi-Fi v kavárně a tak provést takovýto útok je poměrně nákladné, útočník by totiž musel ovládnout routery po cestě mezi vaším WordPressem a aktualizačními servery. Podpisy navíc nepomohou pokud někdo napadne samotný aktualizační server, to si pak upravenou aktualizaci může podepsat sám útočník a WordPress nic nepozná.
Zakladatel WordPressu Matt Mullenweg v článku nakonec uvedl, že podepisování aktualizací někdy v budoucnu přidají, že je to prý dobrý nápad a že může pomoci.
SELECT ... FROM ... WHERE name = %s
Při posílání dotazů do databáze odděluje WordPress dotaz od dat pomocí zástupných znaků jako např. %s
, data správně oescapuje a zástupné znaky jimi nahradí. To brání útoku SQL Injection, při kterém se „z dat“ změní i samotný dotaz. Někdy se takovému způsobu posílání dotazů říká prepared statements.
WHERE id = ?
→ databáze, PHP → 123
→ databáze Jenže prepared statements fungují trochu jinak – posílají dotazy „nadvakrát“. Nejdříve se z PHP na databázový server pošle příkaz PREPARE
se zástupnými znaky, typicky otazníky. Server si připraví vše, co pro spuštění takového dotazu bude potřebovat a vrátí „dotaz připraven“. Teprve poté se dalším příkazem EXECUTE
odešlou data, která server umístí na správné místo v paměti a provede s nimi danou operaci. Vzhledem k tomu, že ve chvíli přijímání dat je na serveru původní dotaz již zparsován a zahozen, tak nelze pomocí dat ovlivnit. Je to naprosto účinná obrana proti SQL Injection. Nevýhodou je dvojnásobná komunikace s databázovým serverem, ale ta se setře ve chvíli, kdy chceme spouštět jeden dotaz s různými hodnotami, stačí pak jen opakovaně volat EXECUTE
. Skutečnému oddělení dat od dotazu na úrovni serveru se občas říká serverové nebo nativní prepared statements.
WHERE id = 123
→ databáze WordPress místo toho používá tzv. emulované prepared statements. Já mám raději termín vázání proměnných, přesněji vyjadřuje způsob práce s dotazy. Vázání proměnných funguje tak, že PHP samo správně ošetří data a nalepí je na určené zástupné místo v dotazu, ve WordPressu jsou to %s
, %d
a další. Poté se na databázový server odešle celý dotaz najednou, včetně správně ošetřených hodnot. Výhoda oproti opravdovým prepared statements je v rychlosti pro neopakující se dotazy – se serverem stač komunikovat jen jednou. Programátor ale ani tak nemusí myslet na ruční escapování, takže je to takový rozumný kompromis.
return mysql_real_escape_string( $string, $this->dbh );
Toto je kus kódu, který se právě o escapování stará. Vázání proměnných je implementováno v metodě prepare()
, najdete ji ve třídě wpdb
. Všimněte si volání vsprintf()
na konci, to je ta funkce, která nahradí zástupné znaky jako %s
za vlastní hodnoty. Celý dotaz včetně hodnot se pak odešle metodou query()
, volanou například z get_row()
a dalších.
\xBF\x5C' UNION ...
Při používání emulovaných prepared statements je i přesto možné provést útok SQL Injection, pokud se nastaví špatně znaková sada. Escapátor (přesněji databázový klient, tedy PHP) nemusí pak vědět, v jaké znakové sadě jsou data, která má ošetřovat. Ve znakové sadě GBK existuje dvoubajtový znak, který končí zpětným lomítkem (hexadecimálně 5C
), takže když se escapováním před apostrof přidá, tak je na serveru zase požrán předchozím bajtem. Server ho „nevidí“, protože je součástí toho dvoubajtového znaku. Podívejte se na můj příklad, který ukazuje správné i špatné nastavení znakové sady. WordPress znakovou sadu nastavuje, ale i tak by bylo lepší, kdyby byla možnost používat nativní prepared statements.
Content-Security-Policy
Content Security Policy (CSP) je vcelku nová věc. No, nová… Původní nápad vznikl v roce 2004, první verze standardu v roce 2012. Pomocí CSP můžete vytvořit seznam zdrojů, které může prohlížeč do stránky načíst, tzv. whitelist. Prohlížeči seznam předáte ve formě HTTP hlavičky, kterou odešlete ze serveru.
Content-Security-Policy: default-src 'self'
Toto je asi nejjednodušší varianta CSP. Direktiva default-src
s hodnotou 'self'
říká, že do stránky se mohou načíst pouze zdroje z aktuální domény, resp. originu. Tedy i kdyby se útočníkovi do HTML kódu podařilo vložit obrázek nebo značku <script src=...>
, tak browser nic takového vůbec nebude stahovat. CSP je druhá úroveň obrany proti Cross-Site Scriptingu a dalším podobným útokům. Když něco zapomenete ošetřit, tak Content Security Policy může vaše uživatele ochránit.
Content-Security-Policy: default-src 'self'; img-src 'self' https://www.google-analytics.com; script-src 'self' 'unsafe-inline'
Takto poslaná HTTP hlavička browseru říká, že defaultně má zdroje načítat jen z aktuálního originu. Pomocí img-src
rozšíříme možnost načítat obrázky navíc z https://www.google-analytics.com
. JavaScript může načíst zase jen z aktuálního originu a navíc může spouštět i tzv. inline skripty, tedy vše, co je mezi značkou <script>
a </script>
, a vše v atributech onclick
a dalších podobných.
Jenže vyjmenovávat všechny zdroje je běh na dlouhou trať, navíc při používání nástrojů jako např. Google Tag Manager dopředu ani neznáme, jaké zdroje do stránky budeme načítat. Nová verze CSP toto řeší, nasazení na nové i existující weby je o dost jednodušší. Zvyšuje se tak šance, že to výrobci webů udělají a tím své návštěvníky zase o něco více ochrání. Bylo by parádní, kdyby WordPress obsah hlavičky Content-Security-Policy
uměl jednoduše vygenerovat nebo nastavit.
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-rAnd0m123'
+ <script src=... nonce="rAnd0m123">...</script>
CSP3 přidává podporu pro 'strict-dynamic'
. Tato hodnota říká, že když už je jednou skript „povolený“, tak může do stránky přidávat další skripty a ty to povolení automaticky zdědí. V případě použití 'strict-dynamic'
je ale dostupná jen jediná možnost, jak skripty povolit: označení pomocí atributu nonce
. Jeho hodnota by měla být jiná pro každé načtení stránky. Tou hodnotou můžeme označit více skriptů v HTML kódu, do CSP hlavičky ji pak přidáme pomocí 'nonce-...'
. Celé si to můžete vyzkoušet na mé stránce s příklady.
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-rAnd0m123' 'unsafe-inline' http: https:; object-src 'none'; report-uri https://report-uri.io/r/...
Univerzální hlavička vypadá takto. Browsery s podporou CSP3 použijí 'strict-dynamic'
a 'nonce-...'
, ty ostatní pak buď jen 'nonce-...'
nebo ty další direktivy. object-src 'none'
zakazuje načítání pluginů, Cross-Site Scripting lze někdy provést i pomocí nich. Prohlížeče s podporou CSP umí navíc posílat informace o tom, kdy a kde byla jaká politika porušena. Pro zobrazování reportů použijte report-uri.com, vygenerujte si tam adresu, na kterou reporty budete posílat a tu pak použijte v direktivě report-uri
.
Content Security Policy vypadá celkem složitě, co? Když to nastavíte blbě, tak se bude taková politika dát obejít. Jenže co to je blbě? Podívejte se na CSP validátor od Google (mají i extenzi pro Chrome), zadejte tam URL stránek nebo rovnou HTTP hlavičku a hned se dozvíte, jak jste na tom. Je tam i příklad bezpečného nastavení.
Možností, jak by se dal WordPress z hlediska zabezpečení vylepšit je mnoho. Na stránce projektu CMS Airship naleznete srovnání bezpečnostních fíčur známých a rozšířených CMS, něco z toho si vyberte a začněte na tom makat. Díky!