CGI skripty jsou náchylné na řadu bezpečnostních děr, bez ohledu na jazyk, který použijete. Tento článek má za cíl uvést základní zásady bezpečného programování CGI skriptů zpracovávajících obsahy formulářů a nějaké tipy, jak se co nejlépe bránit škodolibým návštěvníkům vašich stránek.

I když zde budu uvádět příklady z jazyka Perl, nemění to nic na skutečnosti, že v jiných jazycích musíme při psaní CGI skriptu dodržovat zásady úplně stejné.

Návrh formuláře

Už při psaní formuláře můžeme zamezit některým nepříjemnostem. V definici každého vstupního pole <INPUT> můžeme uvést kromě standardních atributů TYPE, NAME, VALUE a SIZE, také atribut MAXLENGTH. Tento atribut na úrovni zadávání dat do formuláře zajistí, že uživatel nemůže zadat více jak určený počet znaků. Následující příklad ukazuje klasické textové pole pro zadání jména o maximální délce 8 znaků:

<INPUT TYPE=“text“ NAME=“jméno“ VALUE=““ SIZE=8 MAXLENGTH=8>

Když zadáváme hlavičku formuláře <FORM> uvádíme metodu, kterou se mají data z formuláře dostat na vstup našemu CGI skriptu. Máme možnost použít metodu GET nebo POST. Nebudu se teď zabývat detailním popisem a vlastnostmi těchto metod, jenom v krátkosti uvedu, že pokud použijete metodu GET, data z formuláře se předávájí přes proměnnou prostředí $QUERY_STRING, v případě použití metody POST se data předávají na standardní vstup CGI skriptu. Mějme na stránce formulář s výše uvedeným vstupním polem. Nechť skript pro zpracování formuláře se jmenuje zapis.cgi. Pokud data z takového formuláře odešleme pomocí metody GET, v okně prohlížeče v liště LOCATION se objeví například…

http://www/cgi-bin/zapis.cgi?jméno=jeremy

…zatímco při použití metody POST by se zobrazil jenom název CGI skriptu bez jakýchkoliv parametrů:

http://www/cgi-bin/zapis.cgi

Sami vidíte, že metoda POST je lepší, neboť v URL neuvádí žádné bližší informace o struktuře vstupních dat. Samozřejmě, že zkušený uživatel se podívá do zdrojového souboru stránky a tam hravě zjistí názvy polí formuláře. Obecně bych doporučil, pokud vaháte mezi metodou GET nebo POST, volte POST, je o něco bezpečnější. Metodu GET použijte tam, kde metodu POST nelze použít.

Uživatelská data

Po odeslání dat z formuláře se zavolá náš CGI skript, který si zadaná data musí přečíst a nějakým způsobem zpracovat. Pamatujte si jednu z nejdůležitějších zásad pro zpracování vstupních dat z formuláře: Každá hodnota, která přijde z formuláře a kterou nevyléčíme (neověříme), je nakažená!

Jednotlivé hodnoty z formuláře si obvykle uložíme do proměnných. Každou takovou proměnnou, se kterou budeme chtít dále pracovat, musíme zkontrolovat, zda-li neobsahuje nějaké nebezpečné řetězce, a pokud ano, tak takové podřetězce z ní odstranit. Než toto provedeme, říkáme, že náš CGI skript běží prozatimně v tzv. nakaženém režimu (tainted mode). V praxi to znamená, že první řádky CGI skriptu věnujeme detekci a léčení nakažených dat.

Detekce a vyléčení nakažených dat

Jaké tedy máme možnosti a čemu se určitě musíme vyvarovat? Jediným způsobem, jak detekovat nakažená data, je použití regulárních výrazů. Regulární výrazy nám umožňují porovnávat vstupní řetězce s určitými vzory (maskami) řetězců. Např. můžeme zjišťovat, zda-li proměnná $rok obsahuje právě 4 číslice, nebo že proměnná $email je ve tvaru slovo@slovo.doména, kde slovo je řetězec, který obsahuje pouze znaky anglické abecedy, číslice, podtržítko, pomlčku a tečku, a doména pak obsahuje pouze znaky anglické abecedy a má max. délku 3 znaky. Regulární výrazy mají velké využití a jsou velmi silným (a především cenným) nástrojem nejen pro CGI programování.

Mějme CGI skript, kterému na vstup jdou následující hodnoty z formuláře: jméno a příjmení, rok narození, e-mail a url. Začátek CGI skriptu v Perlu, ve kterém bychom data od uživatele uložili do proměnných, by mohl vypadat takto:

#!/usr/bin/perl
print „Content-Type: text/html; charset=iso-8859-2\n\n“;
use CGI ‚:all‘;
$jmeno = param(‚jméno‘);
$prijmeni = param(‚příjmení‘);
$rok = param(‚rok‘);
$posta = param(‚email‘);
$web = param(‚url‘);

Máme dva možné postupy, jak s těmito proměnnými (v Perlu nazývanými skaláry) můžeme naložit. Napíšeme si funkci, která nám vrátí pravdivou hodnotu v případě, že proměnná obsahuje nepovolené znaky:

sub is_tainted {
   return ($_[0] =~ s/[^\w\d_\.]//g);
}

Tato funkce vezme první argument a projde jeho celý obsah a narazí-li na nějaký nepovolený znak (tj. různý od základních abecedních znaků, číslic, podtržítka a tečky), tak jej vymaže. Počet vymazaných nepovolených znaků je pak touto funkcí vrácen. Pokud bylo provedeno alespoň jedno zrušení znaku, znamená to, že funkce vrací pravdivou hodnotu. Zároveň máme jistotu, že argument již není nakažený. Takže můžeme napsat např.:

if (is_tainted($jmeno)) {
   print „\$jmeno byla nakažená, nyní je vyléčená a obsahuje: $jmeno\n“;
}
else {
   print „\$jmeno je v pořádku\n“;
}

Druhý způsob využijeme např. u proměnných $rok a $email. Ten spočívá v tom, že přesně nadefinujeme „vzor“, kterému zadaný řetězec musí odpovídat. Např. $rok může obsahovat právě 4 číslice a nic jiného:

if ($rok =~ /^\d{4}$/) {
   print „\$rok je v pořádku\n“;
}
else {
   print „\$rok je nakažená, její obsah ničím\n“;
   $rok = 0;
}

Zápis \d reprezentuje cifru, {4} znamená 4 opakování předchozího znaku, a znaky ^ a $ znamenají, že výraz se musí shodovat přesně od začátku až do konce, tj. kromě čtyř cifer tam nesmí být už nic jiného. Kdybychom chtěli zkontrolovat e-mailovou adresu, provedli bychom to následovně:

if ($email =~ /^[\w_\.\d]+\@[\w_\.\d]+\.\w{2,3}$/) {
   # můžeme poslat e-mail na adresu $email
   # i když samozřejmě nemáme jistotu, že daný e-mail existuje
   …
}
else {
   # nemůžeme poslat nic, nevíme kam a jak by co došlo
   …
}

Proměnné prostředí

V každém spuštěném CGI skriptu máme k dispozici celou řadu proměnných prostředí. Jednou z nich je např. již zmiňovaná $QUERY_STRING, dále jsou zde proměnné, ze kterých můžeme zjistit spoustu informací: jméno počítače, ze kterého uživatel přistupuje, IP adresu, typ metody, cestu ke skriptu, verzi webového serveru a spoustu jiných. Jednou z důležitých je proměnná PATH. Je dobré tuto proměnnou vynulovat a pokud v CGI skriptu spouštíme nějaké externí programy, vždy je uvedeme s absolutní cestou. Možná vám to připadne zbytečné, ale důvod je velmi jednoduchý. Nic nebrání uživatelovi, aby svými daty z formuláře doplnil nějakou cestu ke svému prográmku, který se bude jmenovat např. „echo“. Vy pak ve skriptu budete někde nějaké echo volat, a protože obsah proměnné PATH je změněn, může se stát, že se zavolá to nové echo, které může pak napáchat hodně škody. Správný postup je tedy následující:

$ENV{$PATH} = „;

system („/bin/echo ahoj“);

Nejnebezpečnější díry v CGI

Nikdy nedávejte data z formuláře k dispozici shellu, pokud nemáte zaručeno, že data jsou bezpečná. (Já osobně bych doporučoval se následujícím typům příkazů úplně vyhnout. Použijte je jen v nejkrajnějších případech a to tehdy, jste-li si s jejich provedením naprosto jisti.)

open COMMAND, „/usr/bin/finger $adresa|“;
system („/usr/bin/finger $adresa“);
eval „for (\@vsechno) { push (\@neco, \$_) if /$vyraz/; }“;

Nikdy nevytvářejte soubor, jehož název zadává uživatel ve formuláři. Následující příklad je velmi nebezpečný:

open SOUBOR, „>/data/$user_file“;

K tomuto výčtu uvedu ještě malou nevinnou lahůdku. Chceme vyhledat řádky v souboru data.txt, které obsahují zadaný řetězec a jednotlivé řádky chceme uložit do pole @vysledky:

@vysledky = `grep ‚$hledany‘ data.txt`

Zkuste si představit, jaké by to mělo následky, kdyby proměnná $hledany obsahovala řetězec:

; rm -rf / ;

Hlášení chyb

Pokud v CGI skriptu zjisíte, že nějaká vstupní data byla nakažená, je nejlepší se nechat ihned o této skutečnosti informovat. Možností máme několik. Buď si vedeme nějaký soubor error-log.txt, kam se budou všechny chyby zaznamenávat, nebo si necháme poslat okamžitě e-mail. V Perlu máme možnost využít modul Tie::STDERR, který umí standardní chybový výstup poslat e-mailem, např.:

use Tie::STDERR skrivan@centrum.cz, ‚zapis.cgi: Intruder detected‘;

A pak kdekoliv ve skriptu, když narazíme na chybu, můžeme napsat následující kód:

if ($chyba) {
   open ERR, ‚>>/errors/error-log.txt‘;    print ERR „chyba: uživatel zadal chybně proměnnou ROK a
EMAIL\n“;
   close ERR;    print STDERR „chyba: uživatel zadal chybně proměnnou ROK a
EMAIL\n“;
}

Jak příklad ukazuje, nejlepší je kombinace obou dvou přístupů. Jednak zapsat nový řádek do error-logu, a dále poslat e-mail, abychom mohli eventuelně včas zasáhnout. Ukázka slouží jenom pro příklad, v praxi bychom si mohli nechat vypsat podrobnější údaje. V e-mailu většinou budeme mít i kompletní výpis všech proměnných prostředí, ze kterých vyčteme čas, datum, IP adresu, jméno stroje, a další.

Logy

Log je textový soubor, do kterého se evidují všechny přístupy na vaše stránky. Zpravidla jeden řádek odpovídá jednomu přístupu. Pro každý přístup bychom si měli vést následující údaje: jaká stránka byla navštívena, z jaké IP adresy, kým (podaří-li se zjistit jméno uživatele), datum, čas. Jak si můžete udělat jednoduchý logovací skript pro vaše stránky? Velmi jednoduše. Naprogramujme si skript log.cgi, který bude mít jeden parametr id. Ten bude obsahovat identifikaci aktuální stránky. Skript log.cgi bude zobrazovat transparentní gif – průhledný obrázek o rozměru 1×1 pixel. V HTML stránce skript zavoláme následujícím způsobem:

<IMG SRC=“/cgi-bin/log.cgi?id=index.html“ BORDER=0>

Skript log.cgi by mohl velmi zjednodušeně vypadat takto:

#!/usr/bin/perl -T
print „Content-Type: image/gif\n\n“;
use CGI ‚:all‘;
$ENV{PATH} = „;
open GRAF, ‚/img/nic.gif‘;
print while <GRAF>;
close GRAF;
$soubor = param(‚id‘);
$soubor =~ s/[^\w_\.\/\-]//g; # $soubor vyléčíme
$AREMOTE_HOST = remote_host;
$REMOTE_HOST = $1 if $AREMOTE_HOST =~ /^([\w\d\.\-_]+)$/;
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime (time);
   $year = sprintf „%02s“,$year;
   $hour = sprintf „%02s“,$hour;
   $min = sprintf „%02s“, $min;
   $sec = sprintf „%02s“,$sec;
open LOG, ‚>>/logs/august2000/log.txt‘;
$line = join ‚:‘, $soubor, $REMOTE_HOST, „$mday-$mesic-$year“, „$hour-$min-$sec“ ;
print LOG $line,“\n“;
close LOG;

Logy si veďte. Samozřejmě, pokud máte nějaký skriptík na své studentské HTML stránce, tak relativně o nic nejde, ale pokud jste správcem nějakého systému pro banku a někdo se vám do systému nabourá, pak logy jsou to jediné, co může policii při vyšetřování pomoci. Nedávno jsem byl na jedné přednášce, která se týkala ochrany dat a logům se věnovala velká, a dle mého názoru, zasloužená pozornost. Nejednou už logy výrazně pomohly při dopadení pachatele.

Další tipy

Dále byste měli pamatovat na to, že vaše disky nemají neomezenou kapacitu. Takže uživatel, shledá-li, že vaše kontroly neprolomí, mohl by se rozhodnout, že zahltí váš diskový prostor. Budete-li provozovat nějaký seznam linků na různé stránky, tříděné do kategorií, kde uživatelé budou mít možnost přidat vlastní odkaz, je vhodné si zavést do skriptu kontrolní mechanismus, který například nedovolí vložit víc jak 1000 odkazů za den. Kdybyste to nekontrolovali, uživatel může klidně generovat nějaké odkazy a několikrát (řádově v tisících) přidat odkaz, až váš disk bude zcela zahlcen, nebo dojde k překročení kvóty. V takových případech zpravidla obvykle přestane chodit pošta a přestane fungovat řada věcí. Nemusíte nutně o nějaká data přijít, ale způsobí to přinejmenším řadu nepříjemností.

Při kontrole vstupních dat provádějte kontrolu, zda vám uživatel nezadal do políček formuláře HTML kód. Divili byste se, co taková HTML značka dokáže vyvést. A pokud se takové údaje ukládají do nějakých nastavení, které čtou i jiné webovské stránky, může dojít ke zhroucení obsahů či designů vašich www stránek. Můžete si zkusit, co se stane, když doprostřed HTML stránky vložíte např. tagy <HTML>, <HEAD>, <TITLE>, a jiné.

Pokud váš skript pracuje s SQL databází, musíte si dát pozor na to, aby zadaná data uživatelem neobsahovala např. vnořený SQL příkaz. Kdybyste pak hledali nějaké informace z databáze pomocí příkazu SELECT a v jeho těle byste měli vnořen příkaz např. DELETE FROM people, tak byste (za určitých okolností) mohli přijít o data v tabulce PEOPLE. Pokud uživatel zvládá dobře jazyk SQL, byl by schopen napsat takový vnořený příkaz DELETE, který by vám během okamžiku smazal celou vybudovanou strukturu databáze včetně dat (takový příkaz zde z bezpečnostních důvodů psát nebudu :-)

Shrnutí

Co říci závěrem? Buďte důslední v kontrole vstupních údajů, veďte si logy, všechny chyby si zaznamenávejte, nebo posílejte e-mailem. Vymažte vždy obsah proměnné PATH a ke všem programům a souborům přistupujte přes absolutní cestu. Různé typy CGI skriptů budou obsahovat různé druhy kontrol. Pokud budete psát nějaký specifický CGI skript a víte o někom, kdo už má nějaké zkušenosti a praxi v dané oblasti, zeptejte se ho, jestli se s něčím náhodou nesetkal, nebo jestli nemá nějaký dobrý tip.

Dalším bezpečnostním opatřením jsou samozřejmě pravidelné zálohy celého systému. Nejideálnější je, když jsou fyzicky uloženy na jiném stroji, který např. není připojen na Internet. Doporučuji, dle důležitosti a velikosti systému, jednu až čtyři zálohy týdně.

S opatrností nešetřete, někdy je dobré, po napsání CGI skriptu jej hned nezveřejňovat, ale URL poslat pár svým kolegům nebo přátelům na otestování. Kolegové mohou přijít na nějaké malé nenápadné chyby, které vám mohly uniknout.

To je zatím vše, co bych vám zcela zodpovědně doporučil pro psaní bezpečných CGI skriptů a pro ochranu vašich dat na disku. Uvítám vaše připomínky a postřehy. Rád se s vámi o této poměrně důležité problematice pobavím a něco přiučím.

Žádný příspěvek v diskuzi

Odpovědět