PHP+MySQL bezpečnostní minimum


Máte knihu "Tvorba aplikací v PHP a MySQL" už dávno přečtenou a vrháte se právě do programování třetího webu? Tak to je nejvyšší čas na bezpečnostní minimum...

Žirafka naťukla problematiku bezpečnosti webových aplikací. A ačkoli by toto mohl být další díl seriálu o APIs, tak ta pravidla jsou natolik obecná, že si zaslouží vlastní arogantní článek.

Co by měl programátor v PHP stoprocentně znát a bezpodmínečně dodržovat?

Zapomeňte konečně na register_globals!

Co je předáno přes GET, je v globální proměnné $_GET, co je předáno přes POST, je v $_POST a cookies jsou v $_COOKIE. Zapomeňte na to, že PHP kdy nabízelo možnost automatického naplnění proměnných!

Nevěřte nikomu!

Vše co přišlo zvenčí je podezřelé. Zkontrolujte si, zda přijatá data dávají smysl, podezřelé věci zahazujte. Typicky to jsou znaky <, >, apostrofy, lomítka, uvozovky, středníky a zpětná lomítka. Pokud není opravdu důvod, aby tyto znaky v řetězci byly, tak je zahoďte.

Když má uživatel poslat v parametru číslo, uložte si ho do proměnné přes funkci intval. $userid=intval($_GET['userid']); budiž ode dneška vaší mantrou. Hlavně když s tím číslem budete dál pracovat v SQL dotazu.

Pokud je součástí SQL dotazu řetězec zadaný uživatelem, tak mysql_escape_string() je nutné minimum, u textů je lépe použít mysql_real_escape_string(), které ošetří i znaky v národních sadách (samosebou pokud má váš hosting správně nastavené kódové stránky u PHP i u MySQL a vaše aplikace používá stejné kódování, leckde tomu tak není... Pak je lepší použít vlastní "quote escape" rutinu napsanou na míru – ale to už zabíhám příliš do podrobností).

Pokud budete řetězec od uživatele vypisovat do HTML kódu, nezapomeňte na htmlspecialchars(). Sice se nikdo nejmenuje "<script>alert('ahoj!')</script>", ale to neznamená, že se to nemůže objevit coby hodnota parametru JMENO!

Pokud se řetězec z parametru má stát součástí cesty k nějakému souboru, který budete skriptem číst, mazat nebo nedejbože includovat, tak nejprve vyhoďte z řetězce všechny ".." a pokud možno i lomítka (nejsou-li nutná). Napište si funkci na odstranění všech "nerozumných" znaků z řetězce (vše mimo 0-9, a-z a A-Z, možná ještě mezery, tečky a spojovníku) a pokud očekáváte parametr, v němž se nic jiného vyskytovat nemá, tak jej touto funkcí prožeňte.

Zkontrolujte si u uploadnutých souborů, jestli mají tu příponu, kterou očekáváte, dřív, než je uložíte na disk. Dovolit uživatelům nahrát např. "obrazek.gif.sh" je, slušně řečeno, nerozum. Řiďte se raději pravidlem "co není povoleno je zakázáno" než pravidlem "povoleno je vše co není zakázáno".

Nevěřte cookies. Pokud si něco uložíte k uživateli do cookie, tak se vám nakrásně může vrátit něco úplně jiného. Z hlediska bezpečnosti je cookie stejně nespolehlivá jako parametr předaný GETem.

Omezte je! Nadměrná svoboda uživatelům škodí!

Když mažete jeden řádek z databáze, tak se nestyďte přidat za DELETE FROM ... WHERE ... ještě klauzuli LIMIT 1. Stejně tak při update jednoho řádku.

Hešujte!

Pozor na přihlašovací skripty a na údaje o uživatelích. Podívejte se do přihlašovacího skriptu vaší aplikace. Vidíte tam něco jako "where jmeno='$jmeno' and heslo='$heslo'"? Ano? Pánbůh s vašimi uživateli! Pokud si vaše aplikace do databáze ukládá jméno a heslo uživatelů v otevřeném textu, jste idiot a zasloužíte si nakopat, hacknout a zkrachovat. Heslo ukládejte do databáze minimálně jako jeho MD5 hash, jste-li paranoik, tak jako SHA hash.

Nebuďte líní a rozdělte kontrolu uživatelů na víc kroků. Nejprve si udělejte SELECT * FROM users WHERE jmeno='$jmeno' (jméno samosebou předtím ošetřete přes mysql_escape_string()). Pak zkontrolujte, jestli jste dostali právě jeden řádek. Pokud ano, vyzvedněte si údaje a zkontrolujte, zda heslo odpovídá. Stačí obyčejné PHP porovnání: if ($db['heslo'] == md5($_POST['heslo'])) ...

Nezapomeňte, že cokoli, co jde přes otevřený HTTP protokol, může kdokoli přečíst, a pro citlivá data použijte SSL, pokud to lze.

Nezapomeňte, že HTTPS samo ještě nedělá aplikaci bezpečnou!

 

Pamatujte: Největší bezpečnostní problém při vývoji aplikace bývá přehnaně sebevědomý a málo zkušený (chodí to ruku v ruce) vývojář!


Tak jak jste na tom? Všechno tohle víte a automaticky to dodržujete? Gratuluji! Můžete se v komentářích podělit o další bezpečnostní zásady, které máte na skladě?

Dne 10.02.2007

Twittni

Přidej do: asdf.sk StumbleUpon Toolbar Stumble It!

Komentáře

[1] mail() (Martiner ) 10.02.2007, 20:53:55 [X] [D]
Nekontrolování parametrů funkce mail(), za to bych střílel bez výstrahy. Tohle milého programátora vůbec nevzrušuje, protože to není on, kdo maže tuny spamů z fronty...
Nějaké zkušenosti s http://www.hardened-php.net/hardening_patch.14.html případně http://www.hardened-php.net/suhosin/ ?

[2] (Arthur Dent ) 10.02.2007, 21:00:43 [X] [D]
[1] Hezké, hezké, ale ještě jsem to neviděl v akci. A pokud píšu skript, tak je lépe nespoléhat na to, že poběží někde s takovouhle "berličkou".

Jinak - nezkontrolovat parametry funkce mail() je stejné jako nezkontrolovat např. parametr fopen() a spadá to do výše uvedeného "kontrolovat vše co přichází zvenčí".

[3] (Martiner ) 10.02.2007, 22:24:27 [X] [D]
[2] tak ten hardened není pro bastlíře PHP skriptů (aby ho měli jako berličku), ale pro ty chudáky na jejichž železe to běží. Ale asi se na to dívám z jiné strany barikády :)

[4] (Arthur Dent ) 11.02.2007, 05:49:51 [X] [D]
[4] Nojo, ale tady se nebavíme o tom, jak nezprasit PHP skript, ale o tom, čeho se vyvarovat z hlediska bezpečnosti. Nic o veřejném hostingu, kam si kdokoli naprasí cokoli a ostatní se jen diví.

[5] Bezpecnost (--==[FReeZ]==-- - Mail - WWW) 11.02.2007, 17:30:10 [X] [D]
Ja pouzivam spickovy modul pro Apache => mod_security http://www.modsecurity.org/ Jinak by samozrejme mely Apache + php, mysql bezet v chrooted enviromentu a mysql by melo pokud mozno komunikovat pres UNIX-SOCKETS. Cili v mem pripade ten jediny otevreny port je port 80 (httpd), samozrejme pokud mi nekdo proskenuje porty, *nezjisti* verzi meho httpd, ani nezjisti jakou verzi OpenSSH provozuji a jelikoz mysql komunikuje pres UNIX-SOCKETS, nikdo ani nezjisti, ze mi tu vubec bezi.

To, co jsem vypsal vetsina amateru porusuje, zejmena tech amateru, kteri si dovoli nechat bezet Apache + PHP + SQL ve Window$. Staci nahodit nmap a zjistite co u nich bezi, pak se obcas podivate na http://milw0rm.com a pri trose stesti najdete zhruba do tydne zero day exploit a muzete si s jejich serverem delat co chcete...

PS: Kolik lidi vubec upravilo defaultni konfiguraci v php.ini, mysql.conf a httpd.conf ?

[6] (Arthur Dent ) 11.02.2007, 17:37:16 [X] [D]
[5] Děkuji za zajímavé postřehy, ale toto nebylo předmětem článku.

[7] Ještě lépe MD5 hash... (Radek - Mail ) 11.02.2007, 19:00:04 [X] [D]
...hashovat s nějakým timestampem, protože MD5 hash téhož hesla bude stále stejný - nebo to používat jako HTTPS.

Jinak nejvíce zneužívanou chybou skriptu je skutečně nedostatečně ošetřený formulář a funkce mail().

[8] (Arthur Dent ) 11.02.2007, 19:29:08 [X] [D]
[7] V tomto případě probírám "klasické" přihlášení, tedy takové, kdy se posílá heslo z HTML formuláře via http/https a hashuje se až při kontrole. Posílat hashované heslo by bylo možné, ale hashování s timestampem sebou nese problém synchronizace času klient/server.

[9] (Jakub Suchy ) 11.02.2007, 19:41:35 [X] [D]
Modsecurity je berlicka, Suhosin nejlepe s hardened patchem je to, co by mel pouzit spravce serveru. Mod security se da obejit pridanim pouhe mezery pred promennou, ktera proleze do PHP a to si tu mezeru orizne.

Pozor NEpouzivejte mysql_escape_string() prosim! Pouzijte mysql_real_escape_string(). Doporucuji opravit v clanku. Ta prvni je jiz davno prekonana. Nezapomente na kodovani!

[10] (Arthur Dent ) 11.02.2007, 20:21:46 [X] [D]
[9] V případě uživatelského jména, které bývá v naprosté většině bez znaků s diakritikou, mysql_escape_string stačí. Pokud bude povoleno použít v uživatelském jménu znaky s diakritikou, pak si do databáze uložím pro kontrolu hash a budu porovnávat hashe (vzhledem ke zkušenostem s "kompatibilitou" různých verzí MySQL je to lepší než spoléhat se, že to, co pošle PHP v pondělí bude totéž, co MySQL vrátí v úterý).

Psal jsem o escape_string coby "nezbytném minimu", ale raději doplním real_escape do článku.

[11] (zirafka - Mail - WWW) 11.02.2007, 21:54:43 [X] [D]
[9] mysql_escape_strings() je prekonana jen kvuli tomu, ze muze delat problem u dvou a vice bajtovych sikmookych kodovani (ktera samozrejme programujici ctenari Misantropova zapisniku bezne pouzivaji) nebo je tady jeste nejaky duvod pro mysql_real_escape_strings() o kterem nevim?

[12] (Jakub ) 12.02.2007, 11:30:25 [X] [D]
[11] hlavne kvuli tomu. piseme tady o bezpecnostnim minimu. az se dostanes do situace, kdy budes psat aplikaci pro cinany, co udelas pak? :)

[13] (Arthur Dent ) 12.02.2007, 12:15:28 [X] [D]
[11][12] No, v podstatě by problém neměl nastat ani při použití s UTF-8 a čínštinou, protože tři "zásadní podezřelé" znaky mají kódy 22, 27 a 5C a podobné kódy se v UTF-8 neobjevují "náhodně" (jinak řečeno - neobjeví se coby součást vícebajtového znaku jako třeba u UNICODE znaku "G cedilla" s kódem 0x0122). Čiliže a jinými slovy pro naprostou většinu aplikací mysql_escape_string() postačí.

[14] (johno - Mail - WWW) 12.02.2007, 14:54:38 [X] [D]
Dobrá alternatíva za mysql_*escape_string je ešte použitie prepared statements s placeholdermi.

[15] (duf - Mail - WWW) 09.03.2007, 09:14:58 [X] [D]
@Arthur Dent: pekny clanek, je videt, ze z programatorske logiky si stale nevysel.

[16] (wake ) 10.03.2007, 01:13:21 [X] [D]
prihlasovani: challenge response, kde na cli strane hashnu salt|uname|hash(password), kde salt je pro kazdy request (nebo aspon session) unikatni.
cookies: jedine, na co jsou dobre, je ulozeni sessionid (nejlepe tez nejaky hash) a zbytek lokalne v session
PHP: PERL?

[17] (wake ) 10.03.2007, 01:14:37 [X] [D]
[16] samozrejme hashnu salt|hash(password), uname potrebuju v plaintextu pro search. chjo.

[18] (wake ) 10.03.2007, 01:17:34 [X] [D]
prihlasovani: timeoutovat sessiony, odhlasenou session nedovolit znova prihlasit (tj. zapomenout sessid)

[19] (wake ) 10.03.2007, 01:18:51 [X] [D]
session: kontrolovat SESSID podle IP

[20] (illi - Mail ) 14.02.2008, 21:21:01 [X] [D]
[quote]Když má uživatel poslat v parametru číslo, uložte si ho do proměnné přes funkci intval. $userid=intval($_GET['userid']); budiž ode dneška vaší mantrou. Hlavně když s tím číslem budete dál pracovat v SQL dotazu.[/quote]

Je velký rozdíl mezi tím a:
$userid = $_GET['userid'] + 0;
případně jestli je to o hodně pomalejší?

[21] (Arthur Dent [openID] - Mail - WWW) 14.02.2008, 22:13:29 [X] [D]
[20] Nejsem si jist tím, jak je interně toto řešeno, ale předpokládám, že se v takovém případě vnitřně zavolá ekvivalent onoho intval()