6. díl - PHP injection - co je to a co s tím
V minulém dílu seriálu o PHP jsme si vysvětlili soubory a cykly, řekli jsme si něco o cyklech a načítání souborů do proměnných. V předminulém článku jsem se zmínil o tom, že na adresní řádek není spoleh. Teď máme všechen potřebný arzenál po ruce. Jdeme na věc...
Možné hrozby
Do adresního řádku si může kdokoli cokoli ručně napsat, případně může zveřejnit libovolně zkonstruovaný odkaz. Možná si říkáte: no a co? Stránku mi nepředělá a je jeho věc, co se mu v prohlížeči zobrazí, ne? Ovšem jednou se může třeba na vašem oblíbeném fóru objevit vzkaz:
Hele, mrkněte se na Mrakoplašův blog - to je hustý, co?
[url]http://mrakoplas.nu.am/index.php?stranka=http://www.pecko.com/hardcore.html&titulek=Co%20jsem%20delal%20o%20prazdninach[/url]
Zrovna tohle je celkem neškodný vtípek, ale kdyby tak vypadala internetová banka a někdo by si tímhle způsobem vytáhl ze zákazníků přihlašovací údaje, to by byl prů...duch. Této technice útoku na stránky se říká PHP injection a obrana proti ní je základ, bez kterého se neobejdeme.
Jestli pro vkládání obsahu používáte pouze příkazy include a require, máte obvykle o starost míň, protože vkládání z jiných serverů bývá na většině serverů zakázané. Ale kdyby zakázané nebylo nebo pokud soubory načítáte přes file_get_contents nebo něco podobného, čtěte dál.
Další hrozba se skrývá v zobrazení souborů, které jsou nějakým způsobem citlivé, např. soubor .htpasswd obsahující hesla velmi jednoduše zobrazíme v příkladu zminula touto adresou:
domena.cz/index.php?stranka=.htpasswd
Nepomůže nám ani, když budeme v jiné složce, složku může útočník jednoduše změnit takto:
domena.cz/index.php?stranka=../.htpasswd
Útočník se stále nedostane k PHP kódu. Pokud ovšem toto udělá např. u souboru, kterým stahujeme soubory z našeho webu a který obsahuje funkci jako readfile, může si stáhnout dokonce naše zdrojové PHP kódy, zjistí heslo do databáze, emaily a hesla uživatelů, no jéje...
Zabezpečení
Je potřeba nějak zařídit, aby šly zobrazovat jenom ty stránky, u kterých to dovolíme. Způsobů, jak to konkrétně naprogramovat, je nepřeberně, ukážeme si jenom dva nejjednodušší.
1. Natvrdo nakódovaný seznam stránek
Vtip je v tom, že do adresního řádku nepíšeme přímo jména souborů, ale jenom nějaké zástupné identifikátory (teoreticky sice můžou být stejné, ale není to nutné). Ve skriptu potom pomocí příkazu switch určíme, kterému souboru zadaný parametr odpovídá:
switch($_GET['stranka'])
{
case 'motylci': $soubor='motyli.htm';
$titulek='Sbírka motýlů';
break;
case 'zabky': $soubor='zaby.htm';
$titulek='Sbírka žab';
break;
default: $soubor='zakazano.htm';
};
A máme tu nový příkaz: switch. Slouží k rozvětvení běhu skriptu do několika možných cest v závislosti na hodnotě nějaké proměnné, kterou uvedeme v závorce za slovem switch (v našem případě $_GET['stranka']). Zbytek příkazu je uzavřen do složených závorek. Jednotlivé možnosti začínají slovem case, následuje požadovaná hodnota proměnné, dvojtečka a za ní příkazy, které se mají v tomto případě vykonat. Za poslední z nich je potřeba vždycky dát break, který ze switche vyskočí, jinak by se plynule pokračovalo příkazy z následující možnosti, až do konce (což se někdy může hodit, proto to tak je). V našem příkladě jsme si kromě jména souboru nastavili i titulek, abychom ho nemuseli pracně psát do odkazů. Na konec můžete dát slovo default a příkazy, které se mají provést v případě, že hodnota proměnné neodpovídá žádné z uvedených možností. Když tam default není, neprovede se v takovém případě nic. V našem případě jsme si připravili soubor zakazano.htm, ve kterém bude nejspíš nějaké upozornění na chybu v adrese.
Následné využití proměnných $soubor a $titulek už je doufám jasné.
2. Seznam stránek v nějakém samostatném souboru
Switch je jednoduchý a univerzální, ale nutí nás při každém přidání nové stránky přepisovat skripty a tím se pracnost dostává zpátky na úroveň ruční administrace, což nechceme. Výhodnější je mít seznam dovolených jmen uložený v samostatném souboru (nebo v databázi), který si skript přečte a prohledá. Vytvoření takového seznamu je velice jednoduchá záležitost, protože se dá zautomatizovat (například příkazem DIR). Seznam nahrajeme na server (teoreticky to nemusí dělat jenom administrátor, ale pozor na kolize, kdyby dva lidé těsně po sobě nahráli každý svoji verzi souboru) a do hlavního skriptu napíšeme něco ve smyslu:
$auto_detect_line_endings=1;
$jmena=file('seznam.txt',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
if($jmena and in_array($_GET['stranka'],$jmena))
{...v pořádku, stránku můžeme zobrazit...}
else
{...chyba, tuhle stránku v seznamu nemáme...};
Příkaz na druhém řádku načte seznam ze souboru do pole $jmena, kde v každém prvku bude jedno jméno. Funkci file a její parametry jsme si vysvětlili předminule, stejně jako význam toho prvního řádku.
Třetí řádek rozhoduje, jestli stránku zobrazíme nebo ne. První podmínka je, že proměnná $jmena nesmí být prázdná (prázdná by byla, kdyby se seznam z nějakého důvodu nepovedlo načíst) - v PHP stejně jako v C znamená jakákoli nenulová hodnota ekvivalent logického "true", takže pokud v poli aspoň něco je, je to OK. Druhá podmínka je, že zadané jméno stránky musí být v seznamu. Na to použijeme funkci in_array(), která nám to řekne. Její první parametr je hodnota, kterou v poli hledáme, druhý je to pole a výstupem je true (je tam) nebo false (není tam). Když jsou obě podmínky splněny, stránku vložíme.
Ještě trochu teorie k logickým výrazům. Interpret je vyhodnocuje postupně podle závorek a priority operátorů, v případě stejné priority potom zleva doprava. Jakmile je mu jasný celkový výsledek výrazu, vyhodnocování ukončí (stejně jako např. v Pascalu). Ve výše uvedeném příkladu tedy nejdříve vyhodnotí prázdnost/neprázdnost pole $jmena a kdyby mu vyšlo false, nebude už ani volat in_array, protože ví, že by na výsledku toho andu nic nezměnila.
Teoreticky bychom místo in_array mohli pole klasicky (tj. pomocí for nebo foreach) projít prvek po prvku a postupně je porovnávat se zadaným jménem. Ale bylo by to pomalejší, protože by PHP muselo pracně interpretovat cyklus, zatímco předdefinované funkce jsou v něm zakompilované a pracují s daty přímo.
Ještě by to možná chtělo vysvětlení, proč jsme se při načítání seznamu tak moc chtěli zbavit konců řádků. Je to jednoduché - "stranka.htm\n" je úplně jiný řetězec než "stranka.htm", takže by to in_array nevyhodnotila jako shodu.
V praxi se obvykle seznamy stránek neukládají do souboru, ale do databáze, kde je kromě jména souboru rovnou i titulek, jméno autora a celá řada dalších doplňujících metadat. Často tam bývá uložen i samotný obsah stránky. Výhodou databází je, že bezpečně řeší současné přístupy od několika uživatelů, což je pro větší redakční systémy nezbytné. Databáze si popíšeme v dalších dílech tohoto seriálu. Začneme hned v tom příštím, ve kterém si vytvoříme jednoduché počítadlo návštěv.
Komu to dneska nestačilo, může se podívat na alternativní verzi tohoto trojčlánku s pár bonusy navíc. Příště se podíváme na databáze v PHP.


Tisk



