BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3, str. 1 díl 3, Bezpečnost aplikací
5/3.3 BEZPEČNOSTNÍ ASPEKTY JAZYKA PHP
Register_globals Hlavním bezpečnostním rizikem především začínajícího programátora je opomenutí resetování proměnné, když pracuje v prostředí s direktivou register_globals nastavenou na On. To způsobuje, že proměnné z GET, POST a cookies jsou změněny na standardní proměnné skriptu a tím pádem k nerozeznání od jakýchkoliv jiných. Jeden příklad za všechny: {if ($heslo==“123“) $autentizovan=1; } if ($autentizovan) { ... privátní část. } Pokud bude v tomto případě skript volán s parametrem ?autentizovan=1, provede se privátní část i v případě, že heslo bude zadáno špatně. Pro zamezení této srpen 2006
část 5, díl 3, kap. 3, str. 2
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
chyby je nutné před touto částí vyresetovat proměnnou $autentizovan. Obtížně odhalitelnou chybou je přidávání hodnot do pole. Pokud se pole „inicializuje“ pouhým přidáním první hodnoty, např. $pole[]=“123“, mohou být další hodnoty přidány pomocí ?pole[]=456&pole[]=789. Hranaté versus složené závorky
CPG pořadí
Pokud je register_globals nastavena na On, je třeba si dát pozor na další možnost průniku, která pramenní ze zpětné kompatibility pro přístup k jednotlivým znakům řetězce pomocí hranatých závorek, přičemž se mají používat složené. Opět, pokud definujeme pole jeho hodnotami, například $uzivatel[“jmeno“]=“pepa“; $uzivatel[“heslo“]=“123“ a následně budeme tyto hodnoty testovat na odpovídající z $_GET (GET data), můžeme být nemile překvapeni. Pokud bude pole $uzivatel inicializováno parametrem ?uzivatel=nejaky, bude se s $uzivatel při přiřazování jednat tak, jako kdyby šlo o řetězec, nikoliv o pole. Jiné než první znaky hodnot (epa, eslo) se nepoužijí, indexy jméno, heslo budou neplatné a první znak $uzivatel se nastaví na „p, následně na „h“. Následně se ta samá hodnota („h“) bude testovat na jméno a heslo z GET/POST dat. To dopadne pozitivně, pokud budou jméno a heslo jedno písmeno, stejné jako první písmeno hesla. Na jeho uhádnutí se redukuje bezpečnost aplikace. V konfiguraci PHP můžete specifikovat priority jednotlivých zdrojů (cookies, POST, GET). Programátor musí mít na paměti, že zdroj s vyšší prioritou přepisuje ten s nižší, pokud by se proměnné jmenovaly stejně. Odeslat GET data je pro útočníka značně jednodušší než POST.
srpen 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3, str. 3 díl 3, Bezpečnost aplikací
Vedle $_POST, $_GET a $_COOKIE nevěřte ani těm datům, které poskytuje webserver. Některá mohou pocházet od uživatele, např. z Host hlavičky, častěji QUERY_STRING, REQUEST_URI a PATH_INFO.
Důvěra v $_SERVER
Funkce serialize() umožňuje převést jakoukoliv proměnnou na řetězec, nehledě na její strukturu. Častá praktika je, že se tento výsledek ukládá do souboru, pro příští načtení. Ale pozor, pokud použijete nějakou obecnou příponu, řekněme .ser, může obsah kdokoliv získat (obdobně jako třeba .inc apod.). U této techniky se na tento problém často zapomíná. Pokud bude mít soubor příponu .php, problému se ale nezbavíte, citlivá data z proměnné budou stále k dispozici. Jedinou možností je ještě navíc přidat na začátek souboru instrukci die(); ?> a teprve až za ni zapsat vlastní obsah. Při dotazu na .php soubor se provede a žádný obsah se nevrátí. To je pochopitelně třeba ošetřit i při následném čtení, aby unserialize() nevrátila chybu.
Výsledek serialize() v souboru
U všech nedůvěryhodných dat je vhodné zajistit, že do dalšího zpracování půjdou s takovým typem, jaký odpovídá situaci. Především není vhodné, aby se číselné proměnné od uživatele se po skriptu „potulovaly“ jako řetězce, nakonec také kvůli rychlosti. Použijte například přetypování: $id = (int) $_GET[‘id’];. Dále se naučte používat rodinu funkcí ctype, která dokáže efektivně kontrolovat proměnné, zda neobsahují zakázané znaky.
Používejte typy a kontrolujte obsah
Pokud otvíráte soubory, jejichž název čerpáte z proměnných, je dobré se seznámit s nějakými nástroji. Předně každý soubor před otevřením testujte pomocí funkce file_exists(). Dále existuje funkce basename(), která expanduje název souboru na celou lokální cestu. Nakonec je asi nejlepší používat nějaký „whitelist“ povolených souborů.
Nástroje na cesty
srpen 2006
část 5, díl 3, kap. 3, str. 4
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
Podvržený odkaz bez XSS
Je třeba si uvědomit, že značnou škodu může útočník způsobit podobnou technikou jako Cross-Site Scripting (XSS), o kterém jsme psali v obecné části, nicméně bez získání přihlašovacích údajů. O XSS se může čtenář dočíst v každé publikaci, nicméně diskuse o tzv. Cross-Site Request Forgery tak častá není. Princip je přitom podobný: oběť přinutíme vykonat nějaký (nejlépe javascriptový) kód (ale popřípadě i prosté zobrazení obrázku), přičemž výsledek bude mít stejný efekt, jako kdyby na dané adresy (odkazy) uživatel kliknul. Tak se dá způsobit značná škoda v nejednom administračním rozhranní (smazání článků, obsahu). Pokud navíc aplikace není zabezpečena proti útokům od ostatních uživatelů, kteří mají přihlašovací účet (programátor ošetřil jen útoky „zvenku“, což je relativně časté ušetření si práce), dá se takto napáchat mnoho špatného bez vlastního zjištění jakýchkoliv přihlašovacích údajů. Pokud se nám povede podstrčit uživateli javascript, může uživatel volat jednu adresu, resp. příkaz za druhou/druhým, například pomocí neviditelného obrázku. Jak jsme již říkali u XSS, proti tomuto postupu nepomůže zafixování session na jednu IP adresu.
Úvod do kódování
Nástup vícebytového kódování UTF se dotkl celé řady oblastí. Uživatelé většinou mívají tu „čest“ číst špatně zobrazené národní znaky kvůli zpětně či dopředně nekompatibilnímu softwaru, programátoři řeší převody mezi dalšími kódováními. Z hlediska bezpečnosti se změnilo několik faktorů, o kterých je nutné se zmínit trochu šířeji, protože daná problematika značně přesahuje rámec webových aplikací. Nejprve několik poznámek o tom, co to vlastně UTF je.
srpen 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3, str. 5 díl 3, Bezpečnost aplikací
V minulosti byla základní jednotka pro „jeden znak“ navržena jako osmice bitů, tedy byte. Vytvořila se jakási tabulka, která jednotlivým znakům připisovala kódy od 0 do 255. V rámci každého národního prostředí, kde nebyla angličtina oficiálním jazykem, se muselo navrhnout nějaké kódování, tedy vlastně podmnožina znaků z daných 255, kterých se „vzdáme“ a které nahradíme při vykreslování lokálními symboly, jako ě š č ž ý. Pro mnoho jazyků vzniklo několik kódování (pro češtinu například Windows-1250, Latin2, Kamenických), takže se záhy řešily nejen převody mezi různými kódováními ve smyslu mezinárodním, ale i lokálním. To vše pochopitelně zvyšuje náklady na IT a z toho vyústila nutnost nějakého lepšího řešení. Prvotním nápadem je rozšířit byte třeba na 9 bitů. To v praxi není možné, číslo 8 je zabudováno hluboko v každém dílu elektroniky. Je možné přijít s násobkem, tedy vlastně zdvojit znaky - každý by byl reprezentován dvěma byty, 16 bity. Nebo třeba i více, třemi či čtyřmi. Problémem je, že jelikož jednotlivé jazyky pro 99 % komunikace používají pouhých pár znaků, často by stačilo méně než 256 a docházelo by k obrovskému plýtvání (v principu by veškeré texty zdvojnásobily místo, které na disku zabírají). Jediným možným řešením je tedy kódovat jednotlivé znaky proměnným počtem bytů, podle toho, o jaký jazyk jde. Pokud si vzpomenete na výklad o buffer overflows a základních technikách hackování, které jsou v principu jen správným zacházením s textovými řetězci plus uměním „dobře počítat“ znaky a adresy, asi tušíte, že něco takového je tak zásadní zásah do archisrpen 2006
část 5, díl 3, kap. 3, str. 6
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
tektury, že celý obor informační bezpečnosti musí tuto skutečnost reflektovat. Nyní se nám tedy návrh jednotného kódování rozpadá na dvě části. Za prvé vytvoření „pořadí“ jednotlivých znaků, nejlépe všech znaků všech abeced na světě (každému znaku přiřadíme číslo), za druhé je zde vlastní kódování, tedy způsob jak „zakódovat“ tato čísla do bytů, aby nedocházelo k velkému nárůstu objemu. Detaily nejsou pro bezpečnost podstatné, jen popišme logiku: Běžné znaky západní abecedy by měly zabírat jeden byte, čímž by se docílilo stoprocentní zpětné kompatibility. Dále by s narůstající „vzácností“ dalších znaků měly tyto znaky postupně zabírat dva a více bytů. Již nyní se objevují první exotické vlastnosti výsledku: například dvě slova mohou být stejná, ale v různých kódováních budou zabírat různý počet bytů. Vrcholem nebezpečnosti by bylo, kdyby toto bylo možné i v rámci UTF, což naštěstí algoritmus pro zápis znaků vylučuje. UTF hacking
srpen 2006
V UTF často bývají taková data, které jsou i jinak „moderní“, například zapsaná v nějakém dialektu XML. Krátce po rozšíření UTF se mezi hackery začala diskutovat otázka, zda existuje způsob, jak napsat instrukce strojového kódu tak, aby byl výsledkem validní UTF řetězec. Snaha je pochopitelná, uvědomíme-li si obrovský vzestup XML, který takřka do každé aplikace implementuje nějaký XML parser. Ten má v sobě validátor kódování (XML je na chyby v kódování extrémně náchylné), a pokud hackeři chtějí zneužívat chyby v softwaru, který pracuje s XML, musí nějakým způsobem propašovat spustitelný kód, který bude vyhovovat UTF kódování. Důležitost odpovědi na položenou otázku je tak jednoznačná.
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3, str. 7 díl 3, Bezpečnost aplikací
V jednom z minulých čísel legendárního hackerského časopisu Phrack (www.phrack.org) vyšel článek, který se snaží algoritmus převodu strojového kódu do UTF řetězce zkonstruovat. Výsledek sice není absolutní, ale podle všeho se zdá, že odpověď bude kladná, minimálně pro účely hackování. Nyní se ale vraťme zpět k webovým aplikacím a ukažme si, jak zde problémy s kódováním komplikují bezpečnost. Podívejme se na jednoduchý webový skript, pracující s UTF-7 (rozdíl mezi různými variantami UTF pro účely této publikace není podstaný):
UTF a escapování
header(‘Content-Type: text/html; charset=UTF-7’); $vystup = mb_convert_encoding(`<script> alert(‘Prikaz’);`, ‘UTF-7’); echo htmlentities($vystup);
Skript dělá následující. Na první řádce pošle webovému prohlížeči hlavičku, že má očekávat obsah kódovaný v UTF-7. Následně je do proměnné $vystup zapsán jednoduchý javascript. Řekněme, že programátorův editor pracuje třeba v kódování ISO, takže obsah převedeme pomocí mb_convert_encoding do UTF-7 (mb je předpona značící rodinu funkcí pro multibyte kódování). Následně obsah proměnné vypíšeme, a abychom zabránili např. ukradení session, zajistíme bezpečnost pomocí funkce htmlentities, která převede všechny HTML tagy na text. Jaké je ovšem programátorovo překvapení, když je tato jinak používaná bezpečnostní funkce naprosto neúčinná a skript se provede. Proč je tomu tak? Na vině je fakt, že htmlentities, stejně jako jiné funkce podobného druhu, musí řetězci „rozumět“, ve smyslu srpen 2006
část 5, díl 3, kap. 3, str. 8
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
musí rozumět jeho kódování, aby mohly být bezpečnostní změny v něm provedeny. Ale UTF-7 zde není podporováno a ochrana je neúčinná. V praxi nemusíme chodit pro příklad k žádným malým webům v koutech internetu, na variantu této chyby doplatil minulý rok Google ve své aplikaci GMail. Trik útočníků spočíval ve faktu, že chyběla definice kódování v hlavičce. Internet Explorer, pokud k tomuto dojde, se ho pokusí automaticky zjistit z prvních 4096 znaků. To skýtá následující možnost: nastavit pomocí XSS kódování na UTF-7 třeba jedním jediným znakem a následně poslat skriptu jakýkoliv zákeřný javascript, opět v UTF-7. Jenže onen skript pracuje v jiném kódování, a i když správně provede všechny bezpečnostní filtry nad vstupem od uživatele, řetězec v UTF-7 projde díky výše zmíněné nepodpoře v mnoha funkcích. Takovýchto skrytých problémů s nefunkčním escapováním (filtrováním) existuje více. Konkrétně PHP (speciálně MySQL) se týká následující z nich. Při vytváření SQL dotazu se dohromady skládá několik řetězců, typicky se do kostry dotazu doplňují data od uživatele. Aby data nemohla měnit kostru, je třeba je opět nějak ohraničit - dělá se to apostrofem - a escapovat, tj. ve vlastním obsahu nějak pozměnit apostrofy, aby nebyly interpretovány jako ohraničení, ale jako obsah. Dělá se to tak, že se před ně vloží zpětné lomítko. Jenže málokdo ví, že GBK (zjednodušená čínština) obsahuje znak, který pro tento účel často používané funkce addslashes() přímo na apostrof konverguje. Jak je to možné? Problém je opět v tom, že addslashes() GBK neumí. Slepě nahradí apostrof za lomítko a apostrof. Jenže lomítko je následně v mulsrpen 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3, str. 9 díl 3, Bezpečnost aplikací
tibyte kódování GBK „spojeno“ s předcházejícím bytem, se kterým „náhodou“ dohromady dávají jeden validní GBK znak. Zbude tak apostrof. Řešením pro tento problém i některé další je používat modernější funkci.
srpen 2006
část 5, díl 3, kap. 3, str. 10 díl 3, Bezpečnost aplikací
srpen 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.1, str. 1 díl 3, Bezpečnost aplikací
5/3.3.1 XSS DETAILNĚJI (PHP)
V oblasti informační bezpečnosti se takřka každý týden objeví nějaká studie ukazující nějakou hrozbu, nějaký trend či směr. Většinou se ukazují počty chyb v různých konkurenčních systémech či počty útoků na ně. Na přelomu září a října roku 2006 jedna taková studie prolétla elektronickými médii s trochu větší mírou povšimnutí, než je obvyklé. Jednalo se o materiál organizace Mitre Corporation, což je americká, de facto vládní agentura (placená ze státních prostředků). Její studie měla za úkol zjistit, jaký druh bezpečnostních problémů nás aktuálně v IT světě nejvíce ohrožuje. Zda se jedná o „profláknutý“ buffer overflow (přetečení bufferu), který můžeme s lehkou mírou sarkasmu označit za všudypřítomný, či o lidský faktor, který se eliminuje ještě hůře. Studie dala na první místo Cross Site Scripting (XSS) - relativně primitivní techniku zneužívání webových aplikací, o které jsme mluvili o pár stránek dříve. To je listopad 2006
část 5, díl 3, kap. 3.1, str. 2
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
docela revoluční výsledek, protože až dosud nikdy nikdo (důležitý) tak vysokou míru nebezpečnosti nepřiřkl věci, která má v porovnání s ostatním způsoby hackování tak „mělký“ odborný podtext. Komentář si můžete přečíst například v médiích Computerworld (http://www.computerworld.com/action/ article.do?command=viewArticleBasic&articleId=9003710), Slashdot (http://it.slashdot.org/article. pl?sid=06/09/25/1440220&from=rss) či ZDNet (http://news.zdnet.co.uk/internet/security/0,39020375,39283373,00.htm). Článek na ZDNetu má drobnou interpretační chybu v titulku „Browser flaws biggest software security risk“. Cross Site Scripting je chybou webové aplikace a souvisí s chybami prohlížečů jen nepřímo v tom smyslu, že některé z nich se dají zneužít k lehčímu či úspěšnějšímu zneužití. To se děje v případě, kdy kontext, ze kterého se provádí útok na danou aplikaci, je jiný, než ve kterém běží aplikace samotná (jako kontext je zde myšlena především doména). V tomto případě prohlížeče z bezpečnostních důvodů odmítnou skriptům poskytnout například hodnoty formulářových polí či adresu nějakého rámu (frame) aplikace. V nerespektování tohoto oddělení ale právě spočívá mnoho chyb prohlížečů, XSS je pak někdy možný i v případech, kdy by tomu jinak bylo zabráněno. Účelem této kapitoly je ukázat jednoduché řešení především pro ty účely, kde se z nějakého důvodu buď zapomnělo bezpečnostní ochranu implementovat, nebo je třeba ji z nějakého důvodu třeba zavést nyní, jednoduchým a účinným způsobem. Typicky, pokud se jedná o cizí aplikaci. Skutečně profesionální programátor, který vyvíjí svůj webový software, může být nucen řešit detailněji, o čemž si také povíme (v tomto listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.1, str. 3 díl 3, Bezpečnost aplikací
smyslu kód odpovídá příslovečné „hrubé záplatě“, která řeší vše několika řádky). Nicméně pokusme se zamyslet nad tím, proč je Cross Site Scripting stále častěji uváděn v souvislosti s vážnými bezpečnostními hrozbami. Hlavním důvodem, bez kterého by o žádnou hrozbu nešlo, je především reálná šance škodit. XSS, přestože jeho vyvolání je jednoduché, zkrátka funguje, lze pomocí něj pronikat do cizích účtů a různě zcizovat identitu ostatních uživatelů. Stejně tak lze způsobit velké škody provozovateli, ať už na pověsti, nebo na aktuálních datech. XSS můžeme sice považovat za „hříčku“, ale výsledky zneužití mohou být vážné. Druhým důvodem je fakt, že toto zneužití je i začínajícím webovým programátorům jaksi „na dosah“. Že odborných znalostí pro vyvolání XSS nemusí mít hacker mnoho, jsme již zmiňovali vícekrát. Velkému (ne)bezpečnostnímu potenciálu XSS dále nahrává, že webové aplikace jsou stále populárnější. Obrovské množství služeb, na které jsme kdysi byli zvyklí jen „offline“, se přesouvá na web, a to buď úplně, nebo zčásti - expediční firma například nechá vyrobit pro své zákazníky software, který umožní sledování stavu zásilky na webu. Finálním podpůrným faktorem pro smělé šíření XSS technik je nedbalý přístup zadavatelů. Je třeba si uvědomit, že zatímco běžná firma nakupuje běžný „offline“ software na zakázku jen velmi zřídka a menší či střední společnosti často vůbec, webový software je unikátní produkt od produktu. Pokud má služba nějaké přihlašovací rozhranní, je s výjimkou různých diskusních fór (které komerční subjekty stejně nevyužijí) patrně unikátní. Nyní si uvědomme, v jaké situaci listopad 2006
část 5, díl 3, kap. 3.1, str. 4
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
firma je: převezme webovou aplikaci psanou na míru a kvůli odlišnému zaměření není možnost, jak si může svými vlastními silami ověřit její bezpečnostní kvality. Technické aspekty de facto ani řešit nechce - požaduje, aby vše bez problémů fungovalo. Kvality dodavatelů jsou různé, smluvní odpovědnost často specifikována vágně. Přitom proti XSS je obrana lehká. Potvrzuje se ale známé pořekadlo, že čím je něco lehčí udělat, tím je také lehčí to neudělat, třeba na to zapomenout. Řešení v PHP
Ukažme si hrubou záplatu na problém Cross Site Scriptingu, naprogramovanou v jazyce PHP. Cílem bude, aby se do skriptu nedostaly žádné nebezpečné znaky. Za tímto účelem rekurzivně projdeme všechny proměnné, ve kterých se obsah od klienta nachází. Některé věci necháme obecné (nevyřešené), z důvodu variability účelu. Důvodem pro to je také fakt, že bezpečnostní opatření by neměla být implementována stylem Cut&Paste, což může natropit více škody než užitku. Tedy, nejdříve naprogramujme rekurzivní funkci, která vezme pole referencí. Na každý jeho prvek aplikuje následující: pokud jde o hodnotu, provede filtrování nebezpečných znaků, pokud o pole, pustí na něho sama sebe. Filtrování nechť provádí zatím nespecifikovaná funkce secure(). function safe(&$a) { foreach ($a as $key => $val) if (is_array($val)) safe($a[$key]); else { $a[key] = secure($a[key]) } }
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.1, str. 5 díl 3, Bezpečnost aplikací
Nyní je třeba safe() zavolat na uživatelské vstupy GET, POST, COOKIE a REQUEST. Není třeba volat safe() postupně, udělejme si z této čtyřky pole a zavolejme ji na něj. $A=array(&$_GET,&$_POST,&$_COOKIE,&$_RE QUEST); safe($A); PHP je pro skriptování skutečně výborný jazyk, jednou z jeho předností je obrovský výběr funkcí. Takřka na každou jen trochu používanou operaci s daty existuje funkce (mnohdy si programátoři píší na jednotlivé úkony vlastní rutiny, aniž by věděli, že taková funkce v PHP již existuje). A právě díky tomu bychom mohli safe() radikálně zkrátit. Místo podmínky použijeme rozhodovací přiřazovací výraz a vlastní aplikování filtru provedeme pomocí array_map(). Výsledek bude vypadat přibližně takto: $a = is_array($a) ? array_map(‘safe’, $a): secure($a); A máme jednu řádku. Typické PHP. Všimněte si, že toto zkrácení bude fungovat rekurzivně, přestože array_map rekurzivně nepracuje. Rekurzivita byla zachována opětovným voláním safe(). Jaký je účel rekurzivity? O tom bychom se měli zmínit detailněji. Jde o aspekt, na který zapomínají i zkušení programátoři. Totiž zabezpečit jednotlivé hodnoty, které přicházejí od klienta, profesionál zapomene málokdy. Neuvědomí si ale, že PHP automaticky vytváří ze správně pojmenovaných proměnných pole. Ukažme si případ. Mějme parametry skriptu následující: ?a[]=abcd&a[]=efgh&a[]=ijkl&a[pocet]=3
listopad 2006
část 5, díl 3, kap. 3.1, str. 6
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
Zdá se, že jsme třikrát specifikovali stejnou proměnnou, pokaždé s jinou hodnotou. Chyba! PHP z tohoto pozná, že proměnná $a bude pole. Jelikož první tři prvky nespecifikují žádný index, patrně začne se od nuly. V paměti pak bude toto: $a[0]=“abcd“; $a[1]=“efgh“; $a[2]=“ijkl“; $a[pocet]=3; Co se nyní stane, když na $a aplikujeme nějakou funkci bezpečnostního charakteru, která pracuje s řetězcem? PHP odpoví varovným hlášením, že se pokoušíme pole konvertovat na řetězec (array to string conversion) (na hostinzích bývá vypisování chybových hlášení často vypnuto, takže se toto vůbec nedozvíme). Horší ale bude, že zmíněná funkce se na jednotlivé položky pole $a vůbec neaplikuje a jejich obsah projde do nitra aplikace tak, jak jej uživatel serveru poslal. To je obrovské nebezpečí. Pokud by se obsah vypisoval klientovi v HTML, bylo by možné takto propašovat spustitelný skript. Pokud by se dostal do SQL, šlo by provést SQL injection. Řešení XSS promyslet
Nyní ještě pár slov k příkladu. Jak jsme již psali, některé otázky jsou otevřené. Předně je třeba se zamyslet nad tím, jak provádět vlastní filtrování. Do HTML by se neměly dostat především menšítka a většítka, které uvozují HTML tagy. To ale nestačí. Dále je třeba zamezit propagaci těch uvozovacích znaků, které autor používá pro atributy, tj. nejčastěji uvozovky. Důvodem je možnost navěsit skript na nějakou událost objektu, jak jsme se zmiňo-
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.1, str. 7 díl 3, Bezpečnost aplikací
vali v kapitole o XSS. Pro účely obrany proti SQL injection je nutné escapovat apostrofy, alespoň pokud používáme MySQL. Toto vše řeší například funkce htmlspecialchars(), která převede menšítko na <, většítko na >, uvozovky na " a apostrof na '. A jsme u již zmiňovaného problému. Při ukládání dat do databáze se tak uloží všechny hodnoty poznamenané touto funkcí, což nemusí být vhodné, speciálně v angličtině, kde používáme apostrof častěji. V databázi by měla být data v čisté formě. Obecně by se v programování měly věci implementovat tak, jak jsou funkčně myšleny, nikoliv tak, jak co nejrychleji dosáhnou efektu shodného se správným. Pro účely obrany proti SQL injection se hodnoty escapují jinými funkcemi než pro účely HTML. A to náš skript neřeší a v této podobě může řešit jen velmi špatně. Nicméně zneužití aplikace zabrání a v nových případech stačí. Dále je třeba si promyslet otázku kódování, kterou rozebíráme v souvislosti s UTF-8 na jiném místě. Především pokud aplikaci píšeme v iso-8859-2 či win1250, což jsou populární česká kódování, můžeme narazit na nekompatibilitu některých funkcí, konkrétně již zmíněné htmlspecialchars(). Implementaci funkce secure() ze skriptu tak necháme na čtenáři.
listopad 2006
část 5, díl 3, kap. 3.1, str. 8 díl 3, Bezpečnost aplikací
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 1 díl 3, Bezpečnost aplikací
5/3.3.2 OBRANA PROTI SQL INJECTION
V minulé kapitole jsme si ukázali praktický příklad, jak lze v PHP řešit jeden z nejčastějších bezpečnostních problémů webových aplikací - Cross Site Scripting. Takřka stejným druhem problému je SQL injection, o čemž se zmiňujeme v obecné části publikace. Tyto dvě hackerské techniky tvoří tak trochu nerozlučnou dvojici. Nespojuje je ani tak programátorská či technologická stránka věci, ale jen ta „znalostní“. Zkusit „šťourat“ do nějaké webové aplikace ve snaze vyvolat SQL injection útok je stejně jednoduché jako hrát si s XSS, a hacker tak takřka instinktivně zkusí obě dvě techniky. Ukážeme si proto konkrétněji, jak se mohou programátoři proti SQL injection bránit. Informace budou hodnotné i pro IT manažery - neprogramátory, protože jakožto zadavatelé webových projektů si mohou některé popisované techniky (či jejich ekvivalentní obdoby) vynucovat při sjednávání projektu. A tyto techlistopad 2006
část 5, díl 3, kap. 3.2, str. 2
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
niky (některé z nich), dokáží SQL injection odrazit z principu, úplně a navždy (pokud jsou na všech místech používány, samozřejmě). Zmíníme se také o nové, připravované vlastnosti PHP 6, o databázové vrstvě PDO. Popis problému
Základní problém SQL injection tkví ve faktu, že při konstrukci SQL dotazu se volně zřetězuje řídicí část SQL dotazu s daty. Laicky řečeno, mám-li vybrat z databáze všechny uživatele starší 30 let, a příkazem pro toto má být jeden řetězec, musí v něm být jak sekvence identifikující, že jde o vybrání z tabulky uživatelů a s omezením na věk, tak vlastní hodnota 30. V tomto případě hrozí, že tato hodnota pomocí znaků a slov, vyskytujících se v řídicí části, její smysl naprosto změní. Problémem je, že toto míchání se děje prostým skládáním řetězců „dohromady, jeden za druhým“. Například: $query = “SELECT * FROM uzivatele WHERE vek>’“.$vek.“‘ AND aktivni=1“;. V tomto případě jsme spojili celkem tři řetězce do jednoho ($query). To je v praxi realizováno naprosto jednoduše alokováním další paměti, překopírováním obsahu jednoho řetězce a následně druhého. Řetězce samotné jsou relativně základním typem „objektů“ většiny programovacích jazyků a jejich spojování (konkatenace) je tedy dosti systémová záležitost. Tím pádem nelze nijak smysluplně „navěsit“ na tuto událost nějaké bezpečnostní prvky. Čistě teoreticky si můžeme takovou situaci v některých jazycích představit, ale velmi těžko tak, aby výsledek dobře sloužil našemu účelu, a už vůbec ne v PHP.
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 3 díl 3, Bezpečnost aplikací
Pokud chce programátor ošetřit nebezpečné znaky v proměnné $vek z našeho případu, musí na ni použít nějakou funkci, která zajistí neinterpretování nebezpečných znaků, v tomto případě apostrofu. To je ale docela „šikanující“ úkon, protože se to musí dít při sestavování každého dotazu. Není tedy divu, že mnoho programátorů, kteří se vývojem neživí třeba jen krátce, to často opomíjejí. Jinou variantou je snad jen použití magic quotes, nicméně převažující nevýhody tohoto řešení jsme již probírali. Jak tedy postupovat? Je třeba navrhnout jiný systém pro tvorbu vlastního dotazu. Většina řešení volí syntax známé rodiny funkcí printf()/sprintf(), populární především v jazycích C/C++, ale nacházejících se ostatně i v PHP. Řeší totiž přesně to, co potřebujeme - oddělení formátovacího řetězce od vlastních hodnot. Ukažme si definici funkce a velmi jednoduchý příklad z PHP dokumentace:
Styl sprintf()
string sprintf ( string format [, mixed args [, mixed ...]]) printf(‘There are %d monkeys in the %s’, $num, $location); Jak vidíme, první argument se jmenuje format a je povinný. Následuje řada variabilního počtu argumentů. Ve vlastním volání vidíme jakési znaky %d a %s. Jejich funkce je v určení místa a formátu hodnot, které po formátovacím parametru následují. Tedy na první %_ (kde _ je nějaké písmeno) se vloží první hodnota (tj. druhý parametr, $num), na druhý %_ druhá hodnota (tj. třetí parametr, $location). Podotkněme, že původní význam této funkce nemá s bezpečností absolutně nic společného. Důvod je, že listopad 2006
část 5, díl 3, kap. 3.2, str. 4
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
v kompilovaných jazycích v žádném případě nelze spojovat řetězce s čísly takto jednoduše, jak jsme zvyklí ze skriptovacích jazyků - kompilátor by okamžitě nahlásil chybu. Pro výpis různých hlášení obsahujících proměnné jiných typů je třeba vymyslet nějaký (tento) formátovač. Nyní je již patrně jasné, jak budeme při zabezpečování SQL dotazů postupovat. Totiž úplně stejně. SQL dotaz vytvoříme z formátovacího řetězce a jednotlivých hodnot. Jejich vkládání ale bude zařizovat nějaká funkce, která správně a přesně aplikuje bezpečnostní prvky. Jediné, co se bude lišit, jsou různé způsoby implementace (PHP umožňuje všeho dosáhnout několika technikami...) a pár drobných výjimek a optimalizací pro lepší používání. SafeSQL
Začněme knihovnou SafeSQL. Ta je pod licencí Lesser GPL volně k dispozici na adrese http://www.phpinsider.com/php/code/SafeSQL/. Důvodem, proč stojí za to se jí zabývat, je její relativně profesionální zpracování, na rozdíl třeba od uživatelských komentářů k původní funkci myslq_query() - http://cz2.php.net/mysql_query. Tam se komentující programátoři silně „vyřádili“ - takřka každý druhý příspěvek řeší tento problém, nicméně velmi povrchně. SafeSQL podporuje formátovače %s, %i, %f, %c, %l, %q, %S, %I, %F, %C, %L, %Q, většina z nich kopíruje význam z sprintf() (k tomu se ještě vrátíme). Knihovna je celá objektová (o přínosu nejsem v tomto případě přesvědčen). Jaké jsou formátovací možnosti konkrétněji? Předně je zde %s, značící řetězec, typicky uvnitř apostrofů. Základní celé číslo, integer, značí %i, typicky
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 5 díl 3, Bezpečnost aplikací
naopak mimo apostrofy. Desetinné číslo (float) se vyznačí pomocí %f. Dále tu jsou podstatnější zjednodušení. Číselná množina se vloží pomocí %c, zde si uveďme příklad: $foobar = array(1,2,34,55); $safe_q = $safesql->query( „select * from foo where bar in (%c)“, array($foobar) ); Výsledkem bude dotaz: select * from foo where bar in (1,2,34,55). Hodnoty reprezentované identifikátorem se zavádějí pomocí %l: $foobar = array(‘one’,’two’,’three’,’four’); $safe_q = $safesql->query( „insert into foo (myset) values (%l);“, array($foobar) ); ...výsledek bude: insert into foo (myset) values (one,two,three,four). A nakonec hodnoty reprezentované řetězcem řeší %q: $foobar = array(‘a’,’b’,’c’,’d’,’e’); $safe_q = $safesql->query( „select * from foo where bar in (%q)“, array($foobar) ); S výsledkem: select * from foo where bar in (‘a’,’b’,’c’,’d’,’e’).
listopad 2006
část 5, díl 3, kap. 3.2, str. 6
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
Ukázkové řešení
V úvodu krátké dokumentace autor popisuje příklad, kdy se z tabulky míst vybírá „Fred’s place“, sloupec id musí být v množině „a“, „b“ a „c’s and d’s“ a location má být $location, pokud tato proměnná není prázdný řetězec. PHP kód, zajišťující bezpečné vykonání tohoto dotazu, je relativně složitý. Je třeba ošetřit všechny položky pole, všechny proměnné a vedle vyřešit zahrnutí omezení na Location, pokud $location != „“. Kód vypadá následovně, přičemž vlastní dotaz je poslední část. $sec_name = „Fred’s place“; $section_ids = array(„a“,„b“,„c’s and d’s“); $location = „Lincoln’s best“; if(!empty($location)) { $location_clause = „ and Location = ‘“ . ;addslashes($location) . „‘“; { else } $location_clause = “; } set_type($section_ids, ‘array’); foreach($section_ids as key=$key val=$val) { $section_ids[$key] = addslashes($val); } $query_string = „select * from sections where SectionName =’“ . addslashes($sec_name) . „‘ and id in (‘“ . implode(„‘,’“,slash_array($section_ids)) . „‘) and timestamp >= “ . time() . $location_clause; Jak je vidět, relativně komplikovaná věc a náchylnost na chybu určitě nebude malá. Nyní si vyřešme tento příklad pomocí SafeSQL. Zformování dotazu bude mít taktéž čtyři části, konkrétně nastavení proměnných (1),
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 7 díl 3, Bezpečnost aplikací
vytvoření objektu (2), zapsání formátovacího řetězce (3) a vlastní vygenerování dotazu (4). Ale podívejme se na výsledek: //1 $sec_name = „Fred’s place“; $section_ids = array(„a“,„b“,„c’s and d’s“); $location = „Lincoln’s best“; //2 $safesql =& new SafeSQL_MySQL; //3 $query_string = <<< EOQ select * from sections where SectionName = ‘%s’ and id in (%q) and timestamp >= %i [ and Location = ‘%S’ ] EOQ; //4 $safe_q = $safesql->query( $query_string, array( $sec_name, $section_ids, time(), $location ) ); I neprogramátorovi musí být jasné, že tato konstrukce je minimálně napsaná daleko úhledněji. Všechna možná vnořená volní se vytratila, kostra SQL dotazu je jasná a čitelná. Jediný rozdíl oproti standardnímu SQL je část v hranatých závorkách, která bude vypuštěna, pokud obsah listopad 2006
část 5, díl 3, kap. 3.2, str. 8
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
na místě značky %S bude nulový. Toto je v SafeSQL implementováno jaksi navíc. Použití je závislé na variantě s velkými písmeny (%S, %I, %F, %C, %L, %Q). Také je vidět, že vlastní hodnoty se předávají v druhém parametru pomocí pole, zde by možná bylo estetičtější definovat pole literálem. Jaký je výsledný SQL řetězec? select * from sections where SectionName = ‘Fred\’s place’ and id in (‘a’,’b’,’c\’s and d\’s’) and timestamp >= 987654321 and Location = ‘Lincoln\’s best’ Jak vidíme, všechny apostrofy se správně vyescapovaly pomocí zpětného lomítka, a to ve všech parametrech. Jak již bylo řečeno, knihovnu SafeSQL jsme vybrali díry relativně robustnímu řešení daného úkonu, to je patrné i ze samotné velikosti souboru, více než 9 kb. Nicméně stejně jako při řešení XSS bychom se při řešení bezpečnosti neměli jen tak spoléhat na cizí kód. Ukažme si tedy, jak se k problému postavili někteří jiní programátoři, jejichž funkce můžete najít v příspěvcích na stránce php dokumentace ohledně funkce mysql_select (http://cz.php.net/mysql_query). Nápady budou hodnotné především pro ty programátory, kteří se rozhodnou si podobnou funkci naprogramovat sami. Díky relativně přísným nárokům na kvalitu příspěvků (jejich hodnota je ručně ověřována) se takto dá najít mnoho užitečných funkcí, které nám ušetří čas. Nicméně robustnost příspěvků samozřejmě nemůže být s kompletní knihovnou srovnávána. listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 9 díl 3, Bezpečnost aplikací
Klasické řešení se nabízí pomocí použití regulérních výrazů. Jedna z ukázek implementuje funkci, která generuje SQL dotaz ze zápisu: mysql_query_params( „SELECT * FROM my_table WHERE col1=$1 AND ;col2=$2“, array( 42, „It’s ok“ )) Opět je vidět řešení polem. Samotné nahrazování probíhá vybráním značky $číslo pomocí regulérního výrazu a jeho následné vložení pomocí callback funkce mysql_query_params__callback. \return mysql_query( preg_replace_callback( ‘\$([09]+)/’, ’mysql_query_params__callback’, $query ) ) Samotná callback funkce dostane jako parametr klasické pole „o“ výsledcích hledání regulérním výrazem (viz dokumentace k PHP), takže stačí již jen sáhnout na příslušné místo do druhého parametru mysql_query_params. Před tím je ale ještě provedeno zabezpečení hodnot pomocí cyklu: foreach( $parameters as $k=>$v ) $parameters[$k] = ( is_int( $v ) ? $v : ( NULL===$v ? ;’NULL’ : „‘“.mysql_real_escape_string( $v ).„‘“ ) )
Autor tohoto skriptu měl jistě dobré úmysly, ale jeho řešení postrádá nadhled a v mnoha případech nebude fungovat. Největší problém je, že tato varianta počítá s vynecháním důležitých apostrofů v řídicí části dotazu s tím, že je na svá místa doplní sama. To je ale dost problematické, protože není jasné, v jakých případech se tak má dít a v jakých ne. Autorova podmínka nemusí být dostatečná. Přesněji, příklad předpokládá, že v poli předávaných hodnot budou opravdu jen hodlistopad 2006
část 5, díl 3, kap. 3.2, str. 10
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
noty, nikoliv výrazy, jako například now() či cokoliv, co počítá s hodnotami ve sloupci. Pokud bychom například chtěli vypsat všechny články do dnešního data, podmínka by vypadala takto: datum<now(). Jenže při použití mysql_query_params(„SELECT ... datum<$1„, array(„now()“); dostaneme výsledek SELECT ... datum<’now()’ Apostrofy způsobí nefunkčnost. Bohužel nelze rozhodnout, kdy se mají a kdy nemají použít. Z tohoto důvodu je jejich automatické doplňování minimálně diskutabilní. Nicméně kladem je teoretický (!) poznatek, že pro toto vlastně funkce není určena, řeší se bezpečnost hodnot a veškeré výrazy mají být uvedeny v řídicí části. Za předpokladu programátorova srozumění s tímto faktem je možné použití takto naprogramované funkce doporučit, nicméně samozřejmě nelze zdaleka vyloučit rozčarování v prvním momentu, kdy bude třeba do hodnot výraz dostat (a nebude možné měnit řídicí část). Nabízí se taktéž možnost odlišit výrazy od hodnot v zápisu, tj. v definici příslušného pole při volání funkce. Přidávání apostrofů by tak mohlo být vynecháno v případě, že by obsah byl například uzavřen do hranatých závorek. Mimochodem v příkladu se také testují hodnoty na integer a v případě shody se apostrofa vynechává. Toto je s největší pravděpodobností snaha vyhovět dikci, že vykonání dotazu bude pomalejší, pokud se bude porovnávat číselná hodnota se zápisem v apostrofech. To listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 11 díl 3, Bezpečnost aplikací
je uvedeno v řadě knih o MySQL, ale novější verze obsahují optimalizační postupy, které tuto „vadu“ odstraňují. Ale zpět k problému - z principiálního hlediska se zahrnutí apostrofů v řídicí části dotazu jeví jako výhodnější už ze sémantického hlediska. Jiný autor řeší tento problém použitím různých znaků pro vyznačení míst hodnot. Podíváme-li se na komentář ve skriptu, zjistíme toto: /* Wildcard Rules * SCALAR (?) => ‘original string quoted’ * OPAQUE (&) => ‘string from file quoted’ * MISC (~) => original string (left ‘as-is’) */ Tento autor ještě přidal možnost číst hodnoty přímo ze souboru, což je naprostý nesmysl, respektive nesystematičnost a chyba v návrhu. Ostatní přispěvatelé nabízí řešení buď nezajímavá, nebo shodná s prvním. Problémem všech řešení je jistá omezenost, nikdy nebudeme schopni postihnout všechny případy jako při jednoduchém spojování řetězce. Často se u složitějších dotazů vytváří různé části na různých místech a nemusí mít pevně danou délku. Kdybychom měli postihnout i toto, museli bychom navrhnout nějakou obecnou strukturu-pole, kde by například první položka znamenala řídicí řetězec a všechny další hodnoty. V případě, že by hodnota byla také pole, generování dotazu by se spustilo rekurzivně. Dále by bylo možné implementovat pojmenování vyznačených míst (obdobně jako při pojmenování částí regulérního výrazu). listopad 2006
část 5, díl 3, kap. 3.2, str. 12
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
Je třeba ale poznamenat, že není dobré se dostat do nějakého nestandardního „tweakování“ jazyka, zvláště ne pro tento účel. Takto zásadní operace by měly zůstat čitelné pro co nejvíce programátorů, je tedy dobré se držet toho, co je buď běžné, nebo během krátké chvilky intuitivně pochopitelné. Zvláště když pro tyto účely má PHP v blízké budoucnosti nabízet standardizované postupy. PDO
Špatná standardizace práce s databázemi byla po dlouhou dobu doménou PHP. Jazyk obsahoval podporu pro práci s databázemi jako MySQL, což vedlo programátory k psaní špatně přenosných aplikací, často svázaných na jeden konkrétní databázový stroj. Navíc v poslední době i samotné změny v MySQL způsobily, že některé aplikace nebyly přenosné ani v rámci různých databázových rozhranní MySQL, tj. mysql_* vs. mysqli_. Většina programátorů si dřív či později de facto z donucení napsala nějakou vlastní mezivrstvu pro práci s databází, což je sice krok logický, ale vůči standardizaci naprosto opačný. Je faktem, že ani v podmínce českých hostingových společností v některých případech nelze použít aplikaci bez alespoň nějaké databázové vrstvy, protože se najdou takoví poskytovatelé, kteří omezují použití na rozhranní mysqli_, standardní mysql_ nefunguje. PDO by mělo všechny tyto nešvary vyřešit. Pojetí práce s databázemi je úplně jiné, než na jaké jsou programátoři zvyklí, což je patrné z následujících rozdílů - PDO je psané v C, nikoliv PHP. Tedy nejedná se o žádný framework ve smyslu knihovny v PHP. - PDO nepracuje se standardními databázovými funkcemi, ale pro jednotlivé databázové stroje umožňuje použití takzvaných ovladačů.
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 13 díl 3, Bezpečnost aplikací
- PDO přináší práci s databázemi objektově orientovaný koncept. V současnosti je PDO zahrnuto v PHP 5.1, nicméně implementace je stále označována jako experimentální (ono je totiž v experimentálním stavu i několik ovladačů, konkrétně Oracle, Microsoft SQL Server, Firebird a další, stabilní jsou samozřejmě ovladače pro webové klasiky MySQL a PostgreSQL). Úspěch PDO zatím nelze předpovědět, protože přechod na tak zásadní technologii si nutně vyžádá nějaký čas a tlak z minulosti, tj. zachovat maximum věcí kompatibilních, je vždy silný. Z bezpečnostního hlediska PDO přináší především změnu ve způsobu, jak je (resp. by měl být) konstruován SQL dotaz. Nejprve je dotaz připraven a jsou v něm vyznačena místa, kde budou uživatelské hodnoty. Následně jsou s těmito místy spárovány proměnné a nakonec je dotaz vykonán. Tento způsob je revoluční i v jiných než bezpečnostních ohledech. Ukažme si příklad z dokumentace k PDO. prepare(„INSERT INTO REGISTRY (name, value) VALUES (:name, :value)„); $stmt->bindParam(‘:name’, $name); $stmt->bindParam(‘:value’, $value); // insert one row $name = ’one’; $value = 1; $stmt->execute(); // insert another row with different values $name = ‘two’; $value = 2; $stmt->execute(); ?>
listopad 2006
část 5, díl 3, kap. 3.2, str. 14
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
Jak je vidět, tzv. paceholdery jsou pojmenované syntaxí :jméno. Následně s nimi programátor spároval proměnné $name a $value. Teď přijde změna - toto párování není dosazením, to se děje až při volání metody execute(). Tím pádem každé execute() vykoná SQL dotaz s aktuálními hodnotami. Vlastní hodnoty lze ale specifikovat i jako parametr již zmíněné metody execute(). Dokumentace uvádí následující příklad, ve kterém jsou placeholdery vyznačeny otazníky (tyto dvě metody nelze kombinovat). prepare(“SELECT * FROM REGISTRY where name = ???); if ($stmt->execute(array($_GET[‘name’]))){ while ($row = ;$stmt->fetch()){ print_r($row) } } ?>
Tento koncept je podobný tomu, jaký jsme si sami vytvářeli v předešlé kapitole, pouze se kostra SQL dotazu a data nezapisují na jednom místě. Jak již bylo řečeno, knihovnu SafeSQL jsme vybrali díky relativně robustnímu řešení daného úkonu, to je patrné i ze samotné velikosti souboru, více než 9 kb. Nicméně stejně jako při řešení XSS bychom se při řešení bezpečnosti neměli jen tak spoléhat na cizí kód. Ukažme si tedy, jak se k problému postavili někteří jiní programátoři, jejichž funkce můžete najít v příspěvcích na stránce php dokumentace ohledně funkce mysql_select (http://cz.php.net/mysql_query). Nápady budou hodnotné především pro ty programátory, kteří se rozhodnou si podobnou funkci naprogramovat sami. listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 15 díl 3, Bezpečnost aplikací
Díky relativně přísným nárokům na kvalitu příspěvků (jejich hodnota je ručně ověřována) se takto dá najít mnoho užitečných funkcí, které nám ušetří čas. Nicméně robustnost příspěvků samozřejmě nemůže být s kompletní knihovnou srovnávána. Klasické řešení se nabízí pomocí použití regulérních výrazů. Jedna z ukázek implementuje funkci, která generuje SQL dotaz ze zápisu: mysql_query_params( „SELECT * FROM my_table WHERE col1=$1 AND ;col2=$2“, array( 42, „It’s ok“ )) Opět je vidět řešení polem. Samotné nahrazování probíhá vybráním značky $číslo pomocí regulérního výrazu a jeho následné vložení pomocí callback funkce mysql_query_params__callback. \return mysql_query( preg_replace_callback( ‘/$([09]+)/’, ;’mysql_query_params__callback’, $query ) ) Samotná callback funkce dostane jako parametr klasické pole „o“ výsledek hledání regulérním výrazem (viz dokumentace k PHP), takže stačí již jen sáhnout na příslušné místo do druhého parametru mysql_query_params. Před tím je ale ještě provedeno zabezpečení hodnot pomocí cyklu: foreach( $parameters as $k=>$v ) $parameters[$k] = ( is_int( $v ) ? $v : ( NULL===$v ? ’NULL’ : “‘“.mysql_real_escape_string( $v ).“‘„ ) );
Autor tohoto skriptu měl jistě dobré úmysly, ale jeho řešení postrádá nadhled a v mnoha případech nebude fungovat. Největší problém je, že tato varianta počítá listopad 2006
část 5, díl 3, kap. 3.2, str. 16
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
s vynecháním důležitých apostrofů v řídicí části dotazu s tím, že je na svá místa doplní sama. To je ale dost problematické, protože není jasné, v jakých případech se tak má dít a v jakých ne. Autorova podmínka nemusí být dostatečná. Přesněji příklad předpokládá, že v poli předávaných hodnot budou opravdu jen hodnoty, nikoliv výrazy, jako například now() či cokoliv, co počítá s hodnotami ve sloupci. Pokud bychom například chtěli vypsat všechny články do dnešního data, podmínka by vypadala takto: datum<now(). Jenže při použití mysql_query_params(„SELECT ... datum<$1“, array(„now()“); dostaneme výsledek SELECT ... datum<’now()’ Apostrofy způsobí nefunkčnost. Bohužel, nelze rozhodnout, kdy se mají a kdy nemají použít. Z tohoto důvodu je jejich automatické doplňování minimálně diskutabilní. Nicméně, kladem je teoretický (!) poznatek, že funkce pro to vlastně není určena, řeší se bezpečnost hodnot a veškeré výrazy mají být uvedeny v řídicí části. Za předpokladu programátorova srozumění s tímto faktem je možné použití takto naprogramované funkce doporučit, nicméně samozřejmě nelze zdaleka vyloučit rozčarování v prvním momentu, kdy bude třeba do hodnot výraz dostat (a nebude možné měnit řídicí část). Nabízí se taktéž možnost odlišit výrazy od hodnot v zápisu, tj. v definici příslušného pole při volání funkce. Přidávání apostrofů by tak mohlo být vynecháno v případě, že by obsah byl například uzavřen do hranatých závorek. listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 17 díl 3, Bezpečnost aplikací
Mimochodem v příkladu se také testují hodnoty na integer a v případě shody apostrofy vynechávají. To je s největší pravděpodobností snaha vyhovět dikci, že vykonání dotazu bude pomalejší, pokud se bude porovnávat číselná hodnota se zápisem v apostrofech. To je uvedeno v řadě knih o MySQL, ale novější verze obsahují optimalizační postupy, které tuto „vadu“ odstraňují. Ale zpět k problému - z principiálního hlediska se zahrnutí apostrofů v řídicí části dotazu jeví jako výhodnější, už ze sémantického hlediska. Jiný autor řeší tento problém použitím různých znaků pro vyznačení míst hodnot. Podíváme-li se na komentář ve skriptu, zjistíme toto: /* Wildcard Rules * SCALAR (?) => ‘original string quoted’ * OPAQUE (&) => ‘string from file quoted’ * MISC (~) => original string (left ‘as-is’) */ Tento autor ještě přidal možnost číst hodnoty přímo ze souboru, což je naprostý nesmysl, respektive nesystematičnost a chyba v návrhu. Ostatní přispěvatelé nabízejí řešení buď nezajímavá, nebo shodná s prvním. Problémem všech řešení je jistá omezenost, nikdy nebudeme schopni postihnout všechny případy jako při jednoduchém spojování řetězce. Často se u složitějších dotazů vytvářejí různé části na různých místech a nemusí mít pevně danou délku. Kdybychom měli postihnout i to, museli bychom navrhnout nějakou obecnou strukturu-pole, kde by například první položka znamenala řídicí řetězec a všechny další hodlistopad 2006
část 5, díl 3, kap. 3.2, str. 18
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
noty. V případě, že by hodnota byla také pole, generování dotazu by se spustilo rekurzivně. Dále by bylo možné implementovat pojmenování vyznačených míst (obdobně jako při pojmenování částí regulárního výrazu). Je třeba ale poznamenat, že není dobré se dostat do nějakého nestandardního „tweakování“ jazyka, zvláště ne pro tento účel. Takto zásadní operace by měly zůstat čitelné pro co nejvíce programátorů, je tedy dobré se držet toho, co je buď běžné, nebo během krátké chvilky intuitivně pochopitelné. Zvláště když pro tyto účely má PHP v blízké budoucnosti nabízet standardizované postupy. PDO Špatná standardizace práce s databázemi byla po dlouhou dobu doménou PHP. Jazyk obsahoval podporu pro práci s databázemi jako MySQL, což vedlo programátory k psaní špatně přenosných aplikací, často svázaných na jeden konkrétní databázový stroj. Navíc v poslední době i samotné změny v MySQL způsobily, že některé aplikace nebyly přenosné ani v rámci různých databázových rozhranní MySQL, tj. mysql_* vs. mysqli_. Většina programátorů si dříve či později de facto z donucení napsala nějakou vlastní mezivrstvu pro práci s databází, což je sice krok logický, ale vůči standardizaci naprosto opačný. Je faktem, že ani v podmínce českých hostingových společností v některých případech nelze použít aplikaci bez alespoň nějaké databázové vrstvy, protože se najdou takoví poskytovatelé, kteří omezují použití na rozhranní mysqli_, standardní mysql_ nefunguje.
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.2, str. 19 díl 3, Bezpečnost aplikací
PDO by mělo všechny tyto nešvary vyřešit. Pojetí práce s databázemi je úplně jiné, než na jaké jsou programátoři zvyklí, což je patrné z následujících rozdílů: - PDO je psané v C, nikoliv PHP; tedy nejedná se o žádný framework ve smyslu knihovny v PHP; - PDO nepracuje se standardními databázovými funkcemi, ale pro jednotlivé databázové stroje umožňuje použití takzvaných ovladačů; - PDO přináší práci s databázemi objektově orientovaný koncept. V současnosti je PDO zahrnuto v PHP 5.1, nicméně implementace je stále označována jako experimentální (ono je totiž v experimentálním stavu i několik ovladačů, konkrétně Oracle, Microsoft SQL Server, Firebird a další, stabilní jsou samozřejmě pro webové klasiky MySQL a PostgreSQL). Úspěch PDO zatím nelze předpovědět, protože přechod na tak zásadní technologii si nutně vyžádá nějaký čas a tlak z minulosti, tj. zachovat maximum věcí kompatibilních, je vždy silný. Z bezpečnostního hlediska PDO přináší především změnu ve způsobu, jak je (resp. by měl být) konstruován SQL dotaz. Nejprve je dotaz připraven a jsou v něm vyznačena místa, kde budou uživatelské hodnoty. Následně jsou s těmito místy spárovány proměnné a nakonec je dotaz vykonán. Tento způsob je revoluční i v jiných než bezpečnostních ohledech. Ukažme si příklad z dokumentace k PDO. prepare(„INSERT INTO REGISTRY (name, value) VALUES (:name, :value)“); $stmt->bindParam(‘:name’, $name); $stmt->bindParam(‘:value’, $value);
listopad 2006
část 5, díl 3, kap. 3.2, str. 20
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
// insert one row $name = ’one’; $value = 1; $stmt->execute(); // insert another row with different values $name = ‘two’; $value = 2; $stmt->execute(); ?>
Jak je vidět, tzv. paceholdery jsou pojmenované syntaxí :jméno. Následně programátor s nimi spároval proměnné $name a $value. Teď přijde změna - toto párování není dosazení, to se děje až při volání metody execute(). Tím pádem každé execute() vykoná SQL dotaz s aktuálními hodnotami. Vlastní hodnoty lze ale specifikovat i jako parametr již zmíněné metody execute(). Dokumentace uvádí následující příklad, ve kterém jsou paceholdery vyznačeny otazníky (tyto dvě metody nelze kombinovat). prepare(„SELECT * FROM REGISTRY where name = {?“); if ($stmt->execute(array($_GET[‘name’]))) { while ($row = $stmt->fetch()) { print_r($row); } } ?>
Tento koncept je podobný tomu, jaký jsme si sami vytvářeli v předešlé kapitole, pouze se kostra SQL dotazu a data nezapisují na jednom místě.
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.3, str. 1 díl 3, Bezpečnost aplikací
5/3.3.3 CROSS-SITE REQUEST FORGERY
V prostředí webových aplikací je Cross Site Scripting a SQL injection jistým fenoménem. Takřka v každé příručce (především začínajícího) webového programátora najdete popis těchto chyb a možností jejich zneužití. Patrně díky jisté podobnosti se ale publikace málokdy zmiňují o podobné, dalo by se říci „sesterské“ chybě Cross-Site Request Forgery (CSRF). Rozdíly jsou ovšem znatelné, v technice jak na straně útočníka, tak při obraně webové aplikace. O tom, jak zneužití chyby vypadá, svým způsobem vypovídá název, který přeložen do českého jazyka odpovídá přibližně názvu „podvržení dotazu jinou stránkou“. Princip útoku tedy spočívá v donucení uživatele webové aplikace zavolat klasickou validní URL, typicky nějaké akce administračního rozhranní, která ovšem vyvolá nechtěnou reakci. Může jít o úpravu nějakého obsahu, změnu nastavení či smazání nějaké položky. Cílem tedy nebývá zmocnit se spojení (i když listopad 2006
část 5, díl 3, kap. 3.3, str. 2
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
i to může být možné), ale spíše nějakým způsobem poškodit oběť či aplikaci jako takovou. Nebezpečí CSRF
Nebezpečnost CSRF spočívá v ještě jednodušší proveditelnosti ze strany útočníka a naopak komplikovanější obrany ze strany majitele či provozovatele serveru. Cross-Site Request Forgery je také ukázkovým případem útoku, který je založen jen na posílání dat serveru, nikoliv jejich čtení (ke čtení dat vůbec nedochází). Jak tedy lze donutit uživatele, aby vykonal například jistou akci webového publikačního systému? Velmi jednoduše. Pokud donutíme oběť zobrazit si v prohlížeči stránku, jejíž obsah máme pod kontrolou, existuje nespočet způsobů, jak zavolat cizí adresu. Namátkou třeba pomocí obrázku. Pomůžeme-li si javascriptem, dá se velmi lehce zavolat celá řada na sebe navazujících akcí a tím de facto simulovat kliknutí uživatele, samozřejmě podvodně, v čemž spočívá princip útoku. Skrytí obrázků je nejenže velmi jednoduché, ale dále by patrně šlo bez problémů použít objekt, kterým se v AJAXovém programování HTTP dotazy přímo tvoří. Je evidentní, že útočník musí znát strukturu názvů jednotlivých skriptů aplikace, stejně tak jako jejich parametrů. Pokud by byl útok veden zevnitř organizace, mohl by jakýkoliv uživatel aplikace tyto údaje jednoduše zjistit a následně například donutit kolegu s většími právy donutit provést akci, která je jemu samotnému v systému zakázaná. Druhý typický případ hrozí při použití opensource řešení, kde si potřebné informace může každý zjistit ze zdrojových kódů sám. Pak je velmi snadné naprogramovat skript, který během pár sekund nasimuluje i deset a více podvodných, ale reálných kliknutí, vše v klidu otestovat a až odladěný výsledek použít k samotnému útoku.
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.3, str. 3 díl 3, Bezpečnost aplikací
Proč je ale vhodné se o více méně evidentní a z logiky webového programování vyplývající možnosti zmiňovat v samostatné kapitole? Odpověď je relativně jednoduchá - ze stejné logiky totiž věci plyne, že mnoho standardních obranných prostředků, které jsou již mnohokrát „provařeny“ publikacemi, nefunguje. Posuďte sami. Hlavní technikou obrany proti XSS je pečlivé hlídání řídicích znaků v parametrech jednotlivých skriptů. Ale zde? Zde se žádné nebezpečné znaky nevyskytují, protože uživatel v útočníkově režii posílá správné a validní URL, které třeba vůbec žádné parametry mít nemusí (v extrémním případě). Co podezřelého lze poznat třeba na adrese htttp://www.magazin.cz/admin/smaz_clanek.php?id=123? Vůbec nic. Nebývá bohužel zvykem programátorů zkoumat, zda uživatel klikl na jakési tlačítko smazat, které představovalo tento odkaz, či zda volání pochází z nějaké podvodné činnosti. (Většina vývojářů se soustředí na „hrubou“ obranu proti XSS z minulé kapitoly a CSRF neřeší.) Druhou technikou, která bývá často zmiňována, je zafixování jedné session na konkrétní IP adresu, kterou klient měl v momentu přihlašování. Návod naleznete v příslušné kapitole naší publikace. Bohužel to z evidentních důvodů také nebude fungovat. Obranu je nutné vykonávat správně a důsledně. Pokud není čtenář zběhlý ve webovém programování, nechť si ještě jednou uvědomí, že útok bude veden nikoliv přes prohlížeč útočníka, ale přes prohlížeč oběti. Proto nemůže fungovat omezení na IP adresu, ale na druhou stranu, proto lze využít ověření pomocí HTTP referreru. Ten sice můžeme podvrhnout hned několika způsoby, ale jen na vlastním počítači, prohlížeč oběti referrer bude posílat dál.
Obrana
listopad 2006
část 5, díl 3, kap. 3.3, str. 4
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
díl 3, Bezpečnost aplikací
Nicméně je třeba poznamenat, že tato metoda trochu omezuje výslednou aplikaci. Je třeba odlišit, jestli se pro danou konkrétní stránku bude či nebude ověřování provádět, protože v některých případech jsou dotazy pocházející z kliknutí v kontextu jiné domény žádané. Speciálně v dnešní době se rozmáhá provazování aplikací všemi možnými prostředky a způsoby, takže je třeba ochranu dobře promyslet. Nabízí se například pro velmi citlivé příkazy (především různá mazání v databázích atd.) odpovědět nejprve stránkou s potvrzovacím formulářem. To způsobí, že i kdyby byl CSRF proveden pomocí plovoucích rámů, útočník nebude za standardních podmínek schopen formulář odeslat a dokončit tak požadovanou akci. Zmíněné podmínky zde znamenají, že jsme se opět dostali do „říše“ Cross-Site Scriptingu, tj. pokud je uživatelův prohlížeč v tomto ohledu bezpečný, obrana bude fungovat. Zmíněnou obranu pomocí dodatečného potvrzení lze použít tak, že bude aktivní jen v momentu, kdy je uživatel do aplikace aktivně přihlášen a přesto HTTP referrer obsahuje jinou (či žádnou) adresu. Tak se lehce přiblížíme uživatelově situaci, v momentu přihlášení je regulérní situace „bez referreru“ méně pravděpodobná. Největší nevýhodou řešení bude nutnost ošetření každé akce. Pokročilý způsob obrany
listopad 2006
Lehce sofistikovanější metodu obrany nabízí způsob validace každé adresy pomocí speciálního identifikátoru, který bude nějakým způsobem ověřovat, zda adresu aktuální či minulé stránky vygeneroval server či někdo jiný. To již patří k pokročilým technikám webové bezpečnosti, ke kterým se programátoři ve většině běžných případů neuchylují, nicméně zjednodušenou koncepci si ukázat můžeme.
BEZPEČNÁ POČÍTAČOVÁ SÍŤ
část 5, díl 3, kap. 3.3, str. 5 díl 3, Bezpečnost aplikací
Použijeme programátorský vzor primitivního „elektronického podpisu“ hashovacích funkcí: pokud M je hodnota, kterou chceme podepsat, pak dvojice hodnot (M, h(M+K)) prokáže, že M vytvořil jenom ten, kdo zná klíč K. Tímto způsobem lze „podepsat“ aktuální URL tak, že: - při jejím generování taktéž vypustíme cookie s druhým údajem ze dvojice, - druhý údaj zakomponujeme do nějakého parametru. V obou případech bude údaj sloužit při následujícím HTTP dotazu k ověření „podpisu“ na HTTP referreru. V prvním případě lehce provedeme výpočet, v druhém nejprve z referreru odstraníme parametr (hash je třeba počítat před jeho přidáním). Druhý způsob má nevýhodu v tom, že je třeba (automaticky) upravovat všechny generované odkazy. Na druhou stranu umožňuje takovéto „podepisování“ odkazů zajistit autenticitu zdroje adresy nezávisle na přihlašování, lze tak například poslat odkaz elektronickou poštou. I když po kliknutí bude referrer prázdný, nebude se jednat o CSRF (pokud se ovšem útočník nedověděl tvary adres jinde). U webových aplikací obecně si lze představit velké množství různých způsobů útoků, které sice kombinují tři zde zmíněné způsoby, nicméně případ od případu je implementují lehce odlišně.
listopad 2006
část 5, díl 3, kap. 3.3, str. 6 díl 3, Bezpečnost aplikací
listopad 2006
BEZPEČNÁ POČÍTAČOVÁ SÍŤ