Úvod
K napsání tohoto článku/seriálu mě vedl jeden hlavní důvod, zkušenost s různými programátory v PHP. Ačkoliv jsem kdysi (bohužel) mylně předpokládal, že programovat v PHP umí každý druhý, ani jsem netušil, jak jsem se mýlil. PHP je totiž jazyk, který vychází z Perlu, čímž si z něj odnáší jak výhody, tak i nevýhody. A tou největší nevýhodou PHP je to, že vám umožní dělat věci, které jsou nejenom problematické a neefektivní, ale taktéž velmi nebezpečné.
Právě díky těmto vlastnostem PHP odsuzuje mnoho profesionálních vývojářů, kteří si myslí, že v PHP nelze programovat čistě a bezproblémově. Pro všechny tyto vývojáře i jiné bych rád připravil sérii článků, jež se bude do podrobnosti zaobírat těmi nejzáludnějšími věcmi a funkcemi jazyka. Jež vám ukáže možnosti, jak věci řešit elegantně a správně bez zbytečných problémů.
Bezpečné používání include/require
Nebezpečné a neošetřené vkládání souborů do skriptů v PHP není doménou jen naprostých laiků, ale bohužel i pokročilejších programátorů. Zde ukážu názornou a velmi známou ukázku chybně napsaného šablonovacího systému v PHP:
include "header.php";
include $_GET['page'];
include "footer.php";
Předešlý příklad je relativně dobře známý a obsahuje až příliš mnoho zřejmých bezpečnostních problémů. Pokud například zadáme adresu
skript.php?page=/etc/passwd
, skript nám může vypsat soubor se jmény a hesly uživatelů (samozřejmě pouze pokud PHP neběží v tzv. safe mode a má příslušná oprávnění).
Uvedených chyb si všimnou většinou pokročilejší uživatelé PHP, ale profesionálové najdou chyb mnohem více. Například použití dvojitých uvozovek není nutné, a tím se zbytečně zvyšuje hardwarová náročnost aplikace. Místo dvojitých uvozovek bychom v případě, že vkládáme nijak neformátovaný text, měli zásadně používat uvozovky jednoduché. Dalším, tentokrát již bezpečnostním problémem je použití nesprávné funkce pro vkládání obsahu souboru. Podle mých zkušeností jen málo programátorů v PHP zná rozdíl mezi funkcemi require
a include
. Jak si sami můžete v dokumentaci k PHP přečíst, funkce require
při neúspěchu vrací závažnou (fatal) chybu, jež ukončí běh skriptu, na rozdíl od funkce include
jež hlásí pouze varování. Pokud je tedy soubor pro běh skriptu nezbytný, zásadně byste měli používat funkci require
.
Ale přestaňme se točit okolo „horké kaše“ a podívejme se na zásadní změny ve skriptu, které bychom měli provést.
- Ošetřit vstup
$_GET['page']
před vložením zákařného kódu - Ošetřit vstup
$_GET['page']
před vložením neexistující stránky - Opravit drobné kódovací chyby
1. Ošetření vstupu před zadáním nepovolených znaků
Pro ošetření vstupu před nepovolenými znaky se velmi často používají regulární výrazy. Jejich výhoda tkví v naprosté univerzálnosti použití pro ošetření libovolného vstupu. Zde si ukážeme malý příklad správného ošetření vstupu:
$mypage=eregi_replace('[^0-9a-z\-\_]', '', $_GET['page'];
tip: Pokud vám stačí vkládat jako parametr číslo, doporučuji místo složitých regulárních výrazů používat jednoduché přetypování jako $mypage = (int) $_GET['page'];
, které zajistí, že proměnná $mypage
bude vždy obsahovat pouze číslo
Jak vidíte, výše vstup ošetřuji tak, že jakékoliv nealfanumerické znaky odstraním. Abych ovšem mohl do skriptu vložit jiný soubor, je ještě třeba znát nejenom jeho jméno, ale i plnou cestu k němu. Používání relativních odkazů nedoporučuji.
$mypage=eregi_replace('[^0-9a-z\-\_]', '', $_GET['page'];
$mypage=dirname(__FILE__) . DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . $mypage . '.html';
Předešlý kód ukazuje správné ošetření získání jména souboru ke stránce ./pages/home.html. Pokud se například podíváte pozorněji, zjistíte, že místo lomítka používám konstantu DIRECTORY_SEPARATOR
, která obstarává přechod mezi systémy unix a win32, kde se používají odlišná lomítka. Budete se pravděpodobně velice divit, ale mnoho programátorů právě na tomto místě zbytečně chybuje.
2. Výsledný skript
A zase vracíme k obvyklé chybě mnoha programátorů – při vkládání dat od uživatele nestačí jen ověřit to, jestli neobsahují nebezpečný kód, ale i to, zda jsou pravdivá. Za největší hloupost považuji spoléhání se na to, že vyžadovaná stránka existuje. Pokud tomu totiž tak není, skript jistě vrátí nějakou chybovou hlášku (popřípadě dojde k zastavení jeho vykonávání), což koliduje s tím, co by mělo nastat. Jestliže stránka neexistuje, měli byste přinejmenším odkázat na úvodní stránku, popřípadě zaslat uživateli hlášení 404: Not Found
. Jednoduché ověření existence stránky a také konečný a funkční kód vidíte níže:
require 'header.php';
$mypage=eregi_replace('[^0-9a-z\-\_]', '', $_GET['page'];
$mypage=dirname(__FILE__) . DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . $mypage . '.html';
if(file_exists($mypage)){
require $mypage;
} else {
// error_log("This file doesn't exist: $mypage\n", 3, FILE_ERROR); // případné zaznamenání chyb
require dirname(__FILE__) . DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'index.html';
}
require 'footer.php';
3. Co si zapamatujte
- vždy ověřujte všechny vstupy od uživatele
- místo
include
raději používejterequire
, vyhnete se tak nepředvídatelným situacím, kdy se skript chová v případě neexistence souboru jinak, než bylo požadováno - neúspěšné pokusy o nalezení souboru zaznamenávejte do logu
- pokud přijímáte od uživatele číslo, vždy jej přetypujte
(int) $cislo
- místo lomítek používejte konstantu
DIRECTORY_SEPARATOR
- tam, kde nejsou potřeba dvojité uvozovky, používejte jednoduché
Přetypování
Jak už jsem se výše zmínil, nejsnadnějším způsobem při vstupu čísla je přetypování proměnné na typ int. Ovšem neupozornil jsem vás na potenciální problém, který může přetypování způsobit, podívejte se na skript níže:
$cislo = (int) $_GET['cislo'];
if($cislo == 'ahoj'){
echo 'ok';
}
I když je to zdánlivě nelogické, skript vypíše text „ok“, protože řetězec ‚ahoj‘ se před porovnáním přetypuje také na číslo, tedy 0 == 0, a to platí. Dejte si tedy pozor na to, že v podmínkách se při porovnání řetězce a čísla (int) automaticky konvertuje řetězec a nikoli číslo.
Další informace naleznete na těchto stránkách – www.php.net/language.types
Vkládání SQL kódu
Při dotazech SQL bývá častým problémem fakt, že do dotazů jsou dávána data přímo od uživatelů. Ukážu vám následující jednoduchý příklad opravdu velmi špatně položeného SQL dotazu:
"SELECT $_GET['what'] FROM users WHERE nick = '$_GET['nick']';"
Pokud zavoláme tento odkaz:
skript.php?what=name&id=nick
dostaneme jako výstup jméno uživatele podle jeho přezdívky. Nastává tu ovšem otázka, co se stane, pokud skript zavoláme takto:
skript.php?what=password&id=nick
skript.php?what=name&id=nick`; DROP users;
První příklad vám ve všech databázích vypíše heslo uživatele, druhý příklad vám umožní zahodit celou tabulku uživatelů. Pro ochranu vstupu dat od uživatelů byste měli minimálně vždy pevně formulovat data, která má databáze vrátit (umožnit uživatelům vybrat sloupec tabulky, která obsahuje hesla, například není příliš moudré) a všechny vstupy ošetřovat proti uvozovkám, středníkům apod. Prvnímu příkladu nelze nijak obecně předcházet, jedinou obranou je dobrá kontrola kódu a inteligence programátora, u druhého příkladu je možné dovolit vkládat do databáze pouze alfanumerické znaky, například takto:
function sql($thing) {
if (is_array($thing)) {
$escaped = array();
foreach ($thing as $key => $value) {
$escaped[$key] = $this->sql($value);
}
return $escaped;
}
return mysql_real_escape_string($thing);
}
Předchozí příklad komplexně zpracuje pole se všemi předanými výrazy a umožní jej vložit přímo do SQL. Například můžete použít tento fígl: $dbdata = sql($_POST);
Další informace naleznete na těchto stránkách – www.php.net/mysql_escape_string
Kradení výsledků SQL
Tato technika není moc známá ani moc rozšířená, ale přesto by stálo za to o ní alespoň trochu pohovořit. Pokud používáte tento typ dotazů:
SELECT * FROM users
Vystavujete se riziku, že uživateli může později z nějaké proměnné získat nejenom jména uživatelů, ale i jejich hesla. Proto byste neměli používat vícenásobné výběry, ale pouze přesné dotazy, například:
SELECT username FROM users
Tímto kódem nejenom zvýšíte bezpečnost vašeho skriptu, ale i zvýšíte rychlost jeho provádění, protože z databáze vybíráte pouze ta data, jež jsou pro běh skriptu nezbytná.
Dobré zvyky při úpravách v SQL
Při testování mého PHP kódu se i dříve stávalo, že jsem omylem vymazal celou tabulku údajů. Není to ostatně ani příliš obtížné, stačí jen špatně definovat podmínku, a je smazáno vše, ostatně podívejte se na krásný příklad níže:
DELETE FROM users;
Tento dotaz vám smaže všechny záznamy v tabulce users pouze díky tomu, že jste například vaší funkci zapomněli předat podmínku. Není to nic zvláštního, a ač se to teď nezdá, takovéto chyby v kódu nastávají. Proto vždy, pokud mažete jeden řádek, se limitujte na jeden řádek, například takto
DELETE FROM users LIMIT 1;
Takto ani při špatně postaveném SQL dotazu nemůžete ztratit všechna data. Je více než na pováženou limitovat veškeré SQL dotazy nějakým omezením, a to i v případech, kdy provádíte například příkaz SELECT či UPDATE. Mnohdy byste totiž jinak mohli svou neopatrností způsobit zbytečně velké škody.
Zamaskování PHP
Velmi oblíbenou a také účinnou metodou je zamaskování jména skriptů i jeho proměnných v URL pomocí modulu serveru Apache – mod_rewrite. Změnu přípony souborů z .php
na .html
provedeme přidáním těchto řádků do souboru httpd.conf.
LoadModule rewrite_module modules/mod_rewrite.so
Rewriteengine on
Rewriterule (.+).html$ $1.php
Jak jste si všimli, modul mod_rewrite nahrazuje žádané URL pomocí regulárních výrazů. Zde uvedený příklad je pouze minimální ukázkou možností tohoto velmi silného nástroje, o němž se v dalších dílech určitě zmíníme podrobněji.
Samozřejmě existuje i druhá možnost, a to přiřazení možnosti vykonávání souborům .html, například takto:
AddType application/x-httpd-php .phtml .php3 .php .html
Tato možnost má však zásadní nevýhodu v tom, že obvykle platí pro celý server a znepřehledňuje kompletně organizaci skriptů. Není pak totiž možné určit, které soubory jsou statické, a které dynamické.
Další informace naleznete na těchto stránkách – www.php.net/security.hiding
Co vás čeká příště
Pokud se vám tento seriál po technické stránce zamlouvá, můžete čekat jeho pokračování – ostatně o jeho kvalitách se můžete vyjádřit v diskusním fóru. V pokračování bych měl zájem podívat se na administraci webových aplikací pomocí zrychleného vývoje aplikací. Bavit bychom se měli především o napojení na produkty MS Access či Dadabik, které jsou k tomuto účelu nejvhodnější.