Witryny WWW wykonane w języku PHP wykorzystują adresy URL do przekazywania informacji o wybranych zasobach. Odwiedzając dział Artykuły o adresie index.php?dzial=art użytkownik przekazuje do skryptu index.php zmienną dzial o wartości art. Na tej podstawie aplikacja wyświetli odpowiednią stronę. W artykule przedstawię metodę kontroli zmiennych oraz organizację kodu aplikacji, której zadanie polega na opublikowaniu zawartości bazy danych.
Przykładowa aplikacja
Do przedstawienia metody kontroli danych wykorzystam aplikację zatytułowaną Magazyn INTERNET. Aplikacja ta jest bibliograficzną bazą danych artykułów opublikowanych w Magazynie INTERNET w roku 2003. Strona główna, z opcjami ARTYKUŁY, ROCZNIKI, RUBRYKI oraz PODRUBRYKI jest widoczna na rys. 1.
Opcje menu głównego służą do wyświetlenia wszystkich rekordów danej kategorii. Strona ta przedstawia imiona i nazwiska wszystkich autorów, którzy pisali do MI w roku 2003. Podobnie, opcja ROCZNIKI wyświetla tabelę wszystkich roczników zawartych w bazie danych, opcja RUBRYKI – wszystkie rubryki czasopisma, zaś opcja PODRUBRYKI – wszystkie podrubryki.
Rekordy wyświetlane w tabelach autorów, roczników, rubryk i podrubryk są hiperłączami do szczegółowych danych. Na przykład nazwisko autora jest hiperłączem do strony prezentującej wszystkie artykuły wybranej osoby. W analogiczny sposób nazwa rubryki jest hiperłączem do strony przedstawiającej wszystkie artykuły pochodzące z danego działu. Na tej samej zasadzie:
- rok jest hiperłączem do strony prezentującej wszystkie numery danego rocznika,
- nazwa podrubryki jest hiperłączem do strony prezentującej wszystkie artykuły z danej podrubryki.
Tytuł artykułu jest hiperłączem do strony prezentującej szczegółowe informacje na temat wybranego tekstu. Na stronie pojawią się więc tytuł, lista autorów, wstęp, rubryka i podrubryka, numer czasopisma oraz strony, na których artykuł się znajduje w czasopiśmie.
Rys. 2 przedstawia szczegółowe dane na temat numerów czasopisma.Rysunek pokazuje spis treści jednego konkretnego numeru. Jest to numer 6/2003. Do strony ze spisem treści prowadzą hiperłącza zawierające numer czasopisma, na przykład 8/2003.
Baza danych
Struktura bazy danych wykorzystanej w przykładzie jest widoczna na schemacie. Baza ta zawiera siedem tabel. Kluczem głównym we wszystkich tabelach jest identyfikator: pole o nazwie kończącej się przyrostkiem {stala}_id{/stala}. Jest to ważne przy walidacji.
Na przykład identyfikatorem osoby w tabeli tosoba jest pole {stala}tosoba_id{/stala}, identyfikatorem artykułu w tabeli tartykul – pole {stala}tartykul_id{/stala}, zaś identyfikatorem rubryki w tabeli trubryki – pole {stala}trubryka_id{/stala}.
Adresy URL
Opisana aplikacja stosuje dwie zmienne URL. Pierwsza z nich, o nazwie id, wybiera dział. Przyjmuje ona jedną w dwunastu dopuszczalnych wartości: od 1 do 12. Druga zmienna nazywa się id2. Jej znaczenie jest zależne od wartości id. Zmienna id2 zawiera identyfikator wybranego rekordu. Poprawne wartości zmiennych id oraz id2 przedstawia tabela 1.
Jeśli przyjmiemy, że identyfikatorem osoby Jan Kowalski w bazie danych jest liczba 157 (czyli że wartość pola {stala}tosoba_id{/stala} dla rekordu zawierającego dane Jana Kowalskiego wynosi 157), to wszystkie artykuły Jana Kowalskiego poznamy odwiedzając stronę:
index.php?id=7&id2=157
Natomiast szczegóły artykułu pt. \”multiIMG. Dużo ilustracji zamiast jednej\” poznamy odwiedzając adres:
index.php?id=11&id2=13
artykuł ten ma identyfikator tartykul_id = 13).
Polityka walidacji zmiennych
Zanim przejdziemy do omówienia kodu walidacji zmiennych, zaznaczymy, jakie cele powinny nam przyświecać od samego początku pracy nad witryną.
Sprawdzanie poprawności danych nie jest jedynie dodatkiem, a zasadniczym etapem działania, który musi się znaleźć w każdej aplikacji. Konsekwencją powyższego stwierdzenia jest fakt, że już we wstępnej fazie implementacji witryny należy zwracać uwagę na badanie poprawności zmiennych. Znacznie łatwiej witrynę budować od samego początku uwzględniając walidację danych, niż przerabiać długi kod nie stosujący walidacji.
Drugie spostrzeżenie dotyczy jakości walidacji. Należy zachować dużą ostrożność i do wszelkich danych pochodzących z zewnątrz aplikacji podchodzić nieufnie. Danych nie uznajemy za poprawne, dopóki tego nie stwierdzimy. Pamiętajmy, że zasadę taką należy stosować między innymi w odniesieniu do każdej zmiennej zawartej w adresie URL.
Podejście proponowane przeze mnie w omawianej aplikacji charakteryzuje się następującymi cechami:
- Cała walidacja zmiennych stanowi punkt wejściowy aplikacji. Wszystkie zmienne są sprawdzane na samym początku.
- Na początku zakładam, że dane są błędne. Dopiero, gdy stwierdzę, że wszystkie zmienne są w zupełności poprawne, zmieniam stan aplikacji na poprawny.
- Badam wszystkie możliwe zależności pomiędzy zmiennymi.
Tak wykonywana walidacja gwarantuje, że do aplikacji nie dostaną się żadne złośliwe dane.
Walidacja napisów, a walidacja liczb
Kluczową obserwacją na temat sprawdzania poprawności danych jest fakt, że sprawdzenie poprawności liczb naturalnych jest znacznie łatwiejsze od sprawdzania poprawności napisów. Jak zorganizować strukturę witryny, by sprawdzanie napisów nie było konieczne? To proste. Wystarczy wykorzystać identyfikatory rekordów z bazy danych!
Pamiętajmy więc, że na poziom trudności sprawdzania danych ma wpływ struktura bazy. Nadawajmy rekordom identyfikator w postaci liczb naturalnych, a wówczas nie będzie problemów przy weryfikacji danych.
Wymieniony wcześniej adres artykułu pt. \”multiIMG. Wiele ilustracji zamiast jednej\”:
index.php?id=11&id2=13
wymaga sprawdzenia czy dwie podane zmienne id oraz id2 są liczbami naturalnymi oraz czy ich wartości są dopuszczalne. Główną zaletą takiego rozwiązania jest fakt, że – bez żadnych modyfikacji – tę samą metodę możesz wykorzystać do danych dowolnego rodzaju. W ten sam sposób sprawdzisz czy podany adres odnosi się do istniejącej rubryki, poprawnego rocznika czy numeru czasopisma.
Funkcje str_ievpi() oraz str_ievpifr()
Niestety, w ogromnym zestawie funkcji bibliotecznych języka PHP nie znajdziemy pojedynczej funkcji sprawdzającej poprawność liczby naturalnej, która jest identyfikatorem rekordu w bazie danych. Sprawdzenie takie wymaga:
- kontroli typu danych (zmienne z adresów URL są napisami),
- kontroli długości danych (32-bitowa liczba całkowita nie przekracza wartości 2147483648, ma zatem dziesięć znaków),
- kontroli struktury napisu (może zawierać wyłącznie cyfry),
- wykluczenia zer wiodących.
Zadanie takie wykonuje funkcja {stala}str_ievpi(){/stala} widoczna na listingu 1.Towarzysząca jej funkcja {stala}str_ievpifr(){/stala} dodatkowo sprawdza zakres liczby.
function str_ievpi($ANo)
{
if (
is_string($ANo) &&
(strlen($ANo) <= WALIDACJA_MAX_DL_NAPISU) &&
ereg(\'^(([1-9][0-9]+)|([0-9]))$\', $ANo)
) {
return true;
} else {
return false;
}
}
function str_ievpifr($ANo, $AMin, $AMax)
{
if (
str_ievpi($ANo) &&
($ANo >= $AMin) &&
($ANo <= $AMax)
) {
return true;
} else {
return false;
}
}
Po wywołaniu jednej z dwóch podanych funkcji, zmienną możemy bez ryzyka umieścić w zapytaniu SQL. Nazwy funkcji są skrótami pochodzącymi od \"is exactly valid positive integer from range\".
Szablony zapytań SQL w klasie PEAR::DB
Do dodatkowego zabezpieczenia danych umieszczanych w zapytaniach SQL można wykorzystać klasę PEAR::DB, a w szczególności szablony zapytań. Załóżmy, że chcemy zapytać:
INSERT INTO
tosoba(imie, nazwisko)
VALUES
(\'Jan\', \'Kowalski\')
a dane przechowujemy w zmiennych {stala}$imie{/stala} oraz {stala}$nazwisko{/stala}. Można to zadanie wykonać następująco:
$imie = \'Jan\';
$nazwisko = \'Kowalski\';
$q = \"
INSERT INTO
tosoba(imie, nazwisko)
VALUES
(\'$imie\', \'$nazwisko\')
\";
$db->query($q);
jednak wtedy narażamy się na ataki SQL Injection. Wykorzystując klasę PEAR::DB możemy niebezpieczeństwa uniknąć stosując rozwiązanie:
$imie = \'Jan\';
$nazwisko = \'Kowalski\';
$q = \'
INSERT INTO
tosoba(imie, nazwisko)
VALUES
(?, ?)
\';
$t = array($imie, $nazwisko);
$db->query($q, $t);
Powyższa metoda charakteryzuje się tym, że w miejsce danych umieszczamy w szablonie zapytania znaki zapytania. Następnie przygotowujemy tablicę {stala}$t{/stala}, w której umieszczamy dane. Klasa PEAR::DB zajmie się przygotowaniem zapytania, neutralizując wszystkie niebezpieczne znaki, jakie mogą być zawarte w zmiennych {stala}$imie{/stala} i {stala}$nazwisko{/stala}.
Wprawdzie wszystkie identyfikatory, które będziemy przekazywali do zapytań SQL, będą sprawdzone funkcjami {stala}str_ievpi(){/stala}, jednak metodę powyższą warto zapamiętać i konsekwentnie stosować. Jest to kolejna porcja zabezpieczeń aplikacji.
Struktura kodu sprawdzającego poprawność danych wejściowych
Pierwszą czynnością wykonaną przez aplikację jest ustalenie domyślnie wyświetlanej podstrony serwisu. Jak już powiedziałem, na początku trzeba przyjąć pesymistyczne założenie, że walidacja się nie uda. W takiej sytuacji ujrzymy stronę: \"Strona o podanym adresie nie istnieje\". Witryna taka ma adres:
index.php?id=1
Ponieważ zmienna $akcja określa wybraną podstronę, zatem jedną z pierwszych linijek skryptu jest:
$akcja = 1;
Następnie sprawdzamy, czy wejście na stronę nie nastąpiło po wpisaniu w przeglądarce adresu index.php (bez żadnych zmiennych). Jeśli tak (czyli gdy tablica {stala}$_GET{/stala} jest pusta), to do tablicy {stala}$_GET{/stala} dodajmy zmienną id, by wymusić zmianę adresu z index.php na index.php?id=2 (adres ten jest adresem strony głównej witryny). Odbywa się to w instrukcji:
if (count($_GET) === 0) {
$_GET[\'id\'] = \'2\';
}
Dalej pojawia się duża instrukcja if, która odpowiada za całą walidację. Instrukcja ta sprawdza, czy zmienna id jest zawarta w tablicy {stala}$_GET{/stala} (już teraz taka zmienna musi się tam znajdować) oraz czy wartość zmiennej {stala}$_GET[\'id\']{/stala} jest poprawną liczbą naturalną z zakresu od 2 do 12 (wartość 1 jest zastrzeżona na potrzeby błędów; nigdy nie będziemy w kodzie umieszczali hiperłączy do strony index.php?id=1):
if (isset($_GET[\'id\']) && str_ievpifr($_GET[\'id\'], 2, 12)) {
...
}
Jeśli powyższy warunek nie jest spełniony, to oznacza, że zmienne są niepoprawne i nie mamy czego dalej badać. Jeśli natomiast zmienna {stala}$_GET[\'id\']{/stala} spełnia powyższe warunki, to przechodzimy do konkretnych podstron. Walidacja na każdej z podstron przebiega w sposób dla niej charakterystyczny. Każda z podstron jest walidowana w sposób niezależny w dużej instrukcji switch:
switch ($_GET[\'id\']) {
case 2:
...
case 3:
...
case 12:
...
}
Omówienie poszczególnych przypadków zaczniemy od podstron, które nie mają drugiej zmiennej. Są to podstrony dla których wartość zmiennej id wynosi od 2 do 6, co widać w tabeli. W takiej sytuacji należy sprawdzić, czy tablica {stala}$_GET{/stala} zawiera wyłącznie zmienną id. Do tego wystarczy pojedyncza instrukcja if. Każda z podstron z zakresu od 2 do 6 będzie walidowana w identyczny sposób jak podstrona 2:
case 2:
if (count($_GET) === 1) {
$akcja = 2;
}
break;
Stwierdzenie poprawności danych sprowadza się do ustawienia zmiennej $akcja. Z wartości 1 oznaczającej sytuację błędną przechodzimy do wartości 2, 3, 4, 5 lub 6, w zależności od wybranej podstrony (oczywiście kod ten można zorganizować w postaci pojedynczej instrukcji if; ja preferuję pięć identycznych przypadków case instrukcji switch).
Znacznie ciekawszym przypadkiem są podstrony o numerach od 7 do 12. Na podstronie 7 wyświetlamy listę artykułów wybranej osoby. Wybrana osoba jest identyfikowana na podstawie zmiennej id2 zawartej w adresie:
index.php?id=7&id2=157
gdzie liczba 157 jest identyfikatorem Jana Kowalskiego. Sprawdzenie poprawności wymaga:
- kontroli liczby zmiennych,
- sprawdzenia czy zmienna id2 jest podana,
- sprawdzenia czy zmienna id2 jest poprawną liczbą naturalną,
- sprawdzenia czy zmienna id2 jest poprawnym identyfikatorem istniejącego rekordu w bazie danych.
Powyższe cztery testy wykonamy instrukcją:
case 7:
if (
(count($_GET) === 2) &&
isset($_GET[\'id2\']) &&
str_ievpi($_GET[\'id2\']) &&
$db->DBA_poprawny_id_osoby($_GET[\'id2\'])
) {
$akcja = 7;
}
break;
Pełna kontrola poprawności zmiennej id2 wymaga kontaktu z bazą danych. Jeśli otrzymamy w zapytaniu liczbę id2=1357, to nie możemy stwierdzić czy liczba ta jest poprawna, czy nie. Ona jest poprawną liczbą naturalną, jednak nie wiemy, czy w bazie danych znajduje się rekord o tym identyfikatorze. Jedyną metodą uzyskania odpowiedzi jest wydanie zapytania SQL. Test taki wykonuje metoda {stala}DBA_poprawny_id_osoby(){/stala}, przedstawiona na listingu 3.
function DBA_poprawny_id_osoby($AId)
{
$q = \'
SELECT
tosoba_id
FROM
tosoba
WHERE
tosoba_id = ?
\';
$t = array($AId);
$w = $this->Fdb->getOne($q, $t);
if (DB::isError($w)) {
die(__LINE__ . \' \' . $w->getMessage());
} else {
/*
* $w jest zawsze napisem!
*
*/
if (str_ievpi($w)) {
return $w;
} else {
return false;
}
}
}
W metodzie tej, jak widać, jest stosowana opisana wcześniej technika wykorzystywania szablonów zapytań SQL (mówiąc prościej: zapytanie SQL zawiera znaki zapytania, które są zastępowane zacytowanymi danymi przez klasę PEAR::DB na podstawie tablicy $t).
W podobny sposób przebiega sprawdzenie zmiennych w przypadku pozostałych podstron o numerach 8, 9, 10, 11 i 12. Na podstronie 8 wyświetlamy wszystkie roczniki. Walidacja przebiega następująco (patrz listing 4).
case 8:
if (
(count($_GET) === 2) &&
isset($_GET[\'id2\']) &&
str_ievpi($_GET[\'id2\']) &&
$db->DBA_poprawny_id_rocznika($_GET[\'id2\'])
) {
$akcja = 8;
}
break;
$akcja = 1;
if (count($_GET) === 0) {
$_GET[\'id\'] = \'2\';
}
if (isset($_GET[\'id\']) && str_ievpifr($_GET[\'id\'], 2, 12)) {
switch ($_GET[\'id\']) {
case 2:
if (count($_GET) === 1) {
$akcja = 2;
}
break;
case 3:
if (count($_GET) === 1) {
$akcja = 2;
}
break;
.
.
.
case 7:
if (
(count($_GET) === 2) &&
isset($_GET[\'id2\']) &&
str_ievpi($_GET[\'id2\']) &&
$db->DBA_poprawny_id_osoby($_GET[\'id2\'])
) {
$akcja = 7;
}
break;
case 8:
if (
(count($_GET) === 2) &&
isset($_GET[\'id2\']) &&
str_ievpi($_GET[\'id2\']) &&
$db->DBA_poprawny_id_rocznika($_GET[\'id2\'])
) {
$akcja = 8;
}
break;
.
.
.
}
}
Wnioski na koniec
Jak widać, pełna i poprawna walidacja nie jest wcale trudna. Wystarczy najpierw ponumerować podstrony witryny. W opisanym przykładzie podstrony są ponumerowane od 1 do 12, przy czym strona 1 jest stroną obsługi błędu.
Na początku przyjmujemy, że walidacja się nie udała oraz - w razie gdy tablica {stala}$_GET{/stala} jest pusta - dodajemy zmienną skierowującą wizytę na stronę główną. Następnie sprawdzamy, czy podany identyfikator podstrony (zmienna {stala}$_GET[\'id\']{/stala}) jest poprawny. Jeśli tak, to wykonujemy dużą instrukcję switch, która ma tyle przypadków, ile mamy podstron. Walidacja w każdym przypadku może przebiegać inaczej.
W omawianej aplikacji mamy dwa rodzaje podstron: strony nie wymagające żadnej zmiennej oraz strony zawierające jedną dodatkową zmienną, która jest identyfikatorem odpowiedniego rekordu w bazie danych.
W pierwszym przypadku test sprowadza się do sprawdzenia liczby elementów tablicy {stala}$_GET{/stala}. W drugiej sytuacji należy sprawdzić czy podana liczba jest poprawnym identyfikatorem w bazie danych. Walidacja każdej podstrony stosującej zmienną id2 wymaga przygotowania jednej metody dostępu do bazy danych (np. {stala}DBA_poprawny_id_rocznika(){/stala}).
Główną cechą opisanego rozwiązania jest to, że witryna wyłącznie publikuje zawartość istniejącej bazy danych. Nie umożliwia wprowadzania rekordów za pomocą formularzy.
W takiej sytuacji wszystkie adresy postaci:
index.php?id=xxx&id2=yyy
są drukowane (w kodzie HTML strony) na podstawie zawartości bazy danych. Nie ma zatem prawa pojawić się żadna wartość zmiennej id2, która nie jest poprawnym identyfikatorem rekordu z bazy danych. Możemy wymagać pełnej poprawności wszystkich stosowanych identyfikatorów.
Dobrym nawykiem jest stosowanie wspomnianych szablonów zapytań SQL zaimplementowanych w klasie PEAR::DB. Daje to dodatkowe (pewne) zabezpieczenie przed atakami SQL Injection.
Ostatnią cechą adresów URL, którą moglibyśmy poddać sprawdzeniu, jest kolejność. Poprawnym adresem jest:
index.php?id=xxx&id2=yyy
podczas, gdy adres:
index.php?id2=yyy&id=xxx
świadczy o manipulacji zmiennymi. Taki test jednak, jako nic nie wnoszący, pominąłem. Pominąłem również kolejność wizyt na konkretnych podstronach. W opisanej przeze mnie aplikacji nie ma znaczenia z jakiej podstrony użytkownik trafia na inną podstronę. W niektórych sytuacjach, na przykład na wielostronicowych formularzach, taki test jest konieczny.