W tym artykule postaram się was nauczyć jak zabezpieczyć swoje skrypty PHP przed atakami hackerów oraz jak skonfigurować serwer internetowy Apache i PHP, aby w dużym stopniu podnieść bezpieczeństwo naszych skryptów.
Po przeczytaniu artykułu zdobędziecie wiedzę, która pomoże wam pisać wydajne skrypty pozbawione dziur. Na początku zajmiemy się tzw. register_globals. Nauczymy się jak zapobiegać nadpisywaniu zmiennych, używając w tym celu filtracji danych oraz tablic superglobalnych. Poruszumy także temat bezpieczeństwa wykonywania poleceń systemowych. Następnie poświęcimy chwilę czasu specjalnemu trybowi PHP czyli safe_mode, który zwiększa bezpieczeństwo aplikacji PHP. Zajmiemy się także dyrektywą open_basedir oraz bezpieczeństwem includowania i otwierania plików. Trzeci zagadnienie dotyczyć będzie zabezpieczenia przed używaniem niektórych funkcji oraz poruszony zostanie temat bezpieczeństwa uploadu plików. Na sam koniec zostawimy sobie porady dotyczące pisania skryptów PHP i ich zabezpieczania, czyli takie małe podsumowanie. To wszystko na początek! Zabieramy się do nauki!
register_globals
Gdy dyrektywa register_globals jest włączona wpliku php.ini (ustawione na On), parser PHP zamienia wszystkie typy zmiennych na zmienną globalną tzn. gdy wywołamy np. skrypt w taki sposób index.php?ok=1 to zmienna $id w tym skrypcie będzie miała wartość 1 co się nie powinno zdarzyć. Przeanalizujmy poniższy kod. Zastanówmy się co stanie się gdy wywołamy stronę z parametrem ok=1:
<?php if($pass == „Stachu”)$ok = 1; if($ok == 1){ //KOD ADMINISTRACYJNY } ?> |
Wywołując skrypt index.php?ok=1 mamy dostęp do panelu admina :). Dlatego zalecane jest ustawienie w php.ini dyrektywy register_globals na off i używanie tablic superglobalnych. Zmienne przekazywane do skryptu są podzielone na kilka kategorii, aby programista mógł wiedzieć skąd odebrać dane i jak je traktować. Te kilka kategorii stanowią właśnie tablice superglobalne. Odwołujemy się do nich poprzez $_nazwatablicy[nazwa_zmiennej] np. $_GET[ok]. Poniżej zostały przedstawione możliwe typy danych wejściowych, czyli nazwy tablic superglobalnych:
- $GLOBALS – tablica zawierająca zmienne w zasięgu globalnym skryptu
- $_SERVER – tablica zawierająca zmienne utworzone przez serwer lub związane ze środowiskiem uruchomieniowym danego skryptu
- $_GET – tablica zwierająca zmienne przekazane do skryptu za pomocą metody GET lub adresem URL
- $_POST – tablica zawierające zmienne przekazane do skryptu za pomocą metody POST
- $_COOKIE – tablica zawierająca zmienne dostarczane do skryptu przez ciasteczka
- $_FILES – tablica zawierająca zmienne dostarczane podczas przesyłania plików na serwer
- $_ENV – tablica zawierająca zmienne dostarczane do skryptu przez środowisko operacyjne
- $_REQUEST – tablica będąca alternatywą $_GET i $_POST jednocześnie, przez co nie jest godna zaufania, gdyż może nastąpić przepełnienie zmiennych. Obecność i kolejność dołączania zmiennych do tej tablicy zależy od dyrektywy konfiguracyjnej variables_order z pliku php.ini. Osobiście nie polecam stosowania tej tablicy superglobalnej
- $_SESSION – tablica zawierająca zmienne zarejestrowane jako sesyjne
Jeżeli na naszym serwerze nie możemy wyłączyć obsługi register_globals to należy na początku skryptu ustawić wartość domyślną dla zmiennej w naszym przypadku $ok=0; W naszych skryptach powinniśmy być bardzo rozważni, jeżeli oczekujemy zmiennej przesłanej metodą POST to należy odwołać się do niej za pomocą $_POST[nazwa_zmiennej]
Filtracja danych
Na początku każdej filtracji zmiennej powinno się sprawdzić czy dana zmienna istnieje oraz czy nie jest pusta. Można to zrobić na dwa sposoby. Pierwszym sposobem jest połączenie instrukcji warunkowej if z funkcją isset(), która sprawdza czy dana zmienna istnieje. Funkcja ta pobiera zmienną jako argument i zwraca true, jeżeli dana zmienna istnieje lub false, w przeciwnym wypadku. Drugi sposób to także połączenie instrukcji warunkowej if, lecz tym razem z funkcją empty(), która sprawdza czy zmienna istnieje oraz czy nie ma wartości zerowej bądź pustej i zwraca true, bądź false w przeciwnym wypadku. Wszystkie zmienne pochodzące od użytkownika powinny zostać przefiltrowane. Zmienne te nie powinny zawierać kodu HTML i PHP, choć w niektórych przypadkach lepiej zezwolić na używanie HTML’a, dla wyróżnienia danego tekstu, jednak nie powinny być to takie znaczniki jak: <table>. Powinny one także mieć odpowiednie ograniczenie długości. Przed zapisaniem zmiennych użytkownika do bazy należy poprzedzić wszystkie znaki specjalne jak apostrofy czy cudzysłowy ukośnikiem (\) – metoda unikowa. Funkcje które filtrują zmienne pod względem obecności kodu HTML i PHP to strip_tags() oraz htmlspecialchars(). Pierwsza z nich pobiera ciąg i zwraca go przefiltrowanego, usuwając wszystkie znaczniki HTML i PHP. Druga także pobiera ciąg, lecz zwraca ciąg zamieniając wszystkie znaczniki HTML na zwykły tekst. Wszystkie zmienne pochodzące od użytkownika powinny zostać przefiltrowane (nie tylko niektóre). Funkcja sprawdzająca długość ciągu w PHP to strlen(), pobierająca ciąg znaków jako argument. W połączeniu z instrukcja warunkową if, pozwala na sprawdzenie długości zmiennej. Zmienna powinna mieć radykalne ograniczenia wielkości, tak aby potencjalny włamywacz nie mógł zablokować naszej strony. Aby usunąć znaki specjalne z danego ciągu należy użyć funkcji addslashes(), która poprzedzi wszystkie znaki specjalne ukośnikiem (\), zabezpieczając w ten sposób zapytanie przed „rozsypaniem się”. Po późniejszym wyborze informacji z bazy należy usunąć dodane wcześniej ukośniki używając funkcji stripslashes(). Obie powyższe funkcje pobierają jako parametr ciąg znaków wejściowych i zwracają sformatowany tekst. Przykładowy kod całkowicie filtrujący zmienną przekazującą komentarze ze strony przedstawiono poniżej:
<?php $komentarz = strip_tags($_POST[komentarz]); if(strlen($komentarz) < 255){ $komentarz = addslashes($komentarz); //POLECENIA ZAPISUJĄCE ZMIENNĄ $komentarz W BAZIE! }else{ echo 'Twój komentarz jest za długi!’; } ?> |
Nie tylko zmienna posiadająca długi tekst powinna zostać przefiltrowana, lecz każda zmienna pochodząca od użytkownika powinna być poddana weryfikacji, aby w pełni zabezpieczyć nasz skrypt przed atakiem i jego skutkami.
Bezpieczeństwo wykonywania poleceń systemowych
W PHP istnieją cztery sposoby na uruchamianie własnych poleceń powłoki. Funkcje służące do tego to exec(), passthru(), system() oraz metoda odwróconych apostrofów „. Funkcje te nie powinny zawierać w swoich argumentach danych pochodzących od użytkownika. Każde polecenie powinno być przefiltrowane funkcją escapeshellcmd(), uniemożliwia ona użytkownikom złośliwe wykonywanie poleceń np.
<?php escapeshellcmd(system($_GET[polecenie])); ?> |
Najlepiej jest wyłączyć powyższe metody z PHP dla zwiększenia bezpieczeństwa, w specjalnym trybie safe_mode polecenia te są wyłączone!
Bezpieczeństwo otwierania i includowania plików
Przyjrzyjmy się poniższemu skryptowi, który na niezabezpieczonym serwerze będzie odczytywał plik haseł serwera linuksowego:
<?php $file = file(’etc/passwd’); for($i=0; $i < count($file); $i++){ echo $file[$i].”<br />”; } ?> |
Powyższy skrypt na niezabezpieczonym serwerze powinien wyświetlić wam listę haseł wszystkich użytkowników w systemie (nawet root’a). Co nie powinno mieć nigdy miejsca! Skrypt nie powinien otwierać plików, których sam nie stworzył, bądź nie ma do niego praw! Jak się można przed tym zabezpieczyć? To bardzo proste wystarczy włączyć specjalny tryb PHP safe_mode, bądź odpowiednio ustawić dyrektywę open_basedir w pliku php.ini, jednak o tym w późniejszym czasie. Teraz zajmiemy się includowaniem plików, bezpośrednio z adresu URL. Często w sieci widzimy strony z przykładowym adresem index.php?file=news.php. W skrypcie najczęściej nie ma żadnej filozofii i zawarty jest kod:
<?php include_once($_GET[file]); ?> |
Teraz użytkownik wpisując jako adres index.php?file=/etc/passwd ujrzy… doskonale wiemy co. Jak się przed tym zabezpieczyć oczywiście, jeśli mamy włączone safe_mode lub odpowiednio skonfigurowaną dyrektywę open_basedir nic się nie stanie, w przeciwnym wypadku mamy plik z hasłami. Gdy wykorzystujemy taki mechanizm w naszych skryptach należy na początku sprawdzić czy zmienna $_GET[file] nie zawiera żadnych podwójnych kropek, żadnych ukośników i tym podobnym. Możemy to sprawdzić za pomocą funkcji eregi(). W poleceniu include bądź require powinniśmy także narzucić odpowiedni katalog z którego tylko możemy includować pliki, chociaż jeżeli nie zastosujemy kodu, który sprawdza obecność kropek w zmiennej też to nie wiele pomoże. Najbezpieczniejszym sposobem jest jednak użycie trybu safe_mode, do którego teraz przejdziemy.
safe_mode
Czym właściwie jest ten safe_mode? Otóż safe_mode jest to specjalny tryb parsera PHP, który zwiększa bezpieczeństwo skryptów jak i całego serwera. Tryb ten nie zezwala na dostęp do plików skryptom, które nie są właścicielem danego pliku, czyli nici z odczytywania pliku haseł 🙁 safe_mode ogranicza także dostęp skryptu do zmiennych środowiskowych czy wykonywania poleceń systemowych z poziomu skryptu. Aby włączyć bezpieczny tryb PHP należy ustawić dyrektywę safe_mode w pliku php.ini na On.
open_basedir
Drugą dyrektywą zwiększającą bezpieczeństwo serwera jest open_basedir. Pozwala ona na ograniczenie dostępu do określonego katalogu. Umieszczając ścieżkę w tej dyrektywie uniemożliwiamy dostęp do plików leżących w katalogach powyżej tej ścieżki. Na przykład ustawiając dyrektywę open_basedir na „/home/krzasz/public_html/” nie mamy dostępu do takich plików jak „/etc/passwd” oraz do katalogu „/home/krzasz/”. Jeżeli na serwerze mamy włączoną obsługę open_basedir i safe_mode to najpierw brane są pod uwagę zabezpieczenia przekazywane przez open_basedir, a następnie przez safe_mode. Jeżeli w systemie nie możemy włączyć obsługi safe_mode i open_basedir, to najpierw należy próbować włączyć safe_mode, w przeciwnym wypadku używamy samego open_basedir.
Bezpieczeństwo uploadowania plików
Stwórzmy fomularz, który będzie odpowiedzialny za uploadowanie pliku na serwer o maksymalnej wielkości 500KB:
<form name=”upload” method=”post” enctype=”multipart/form-data” action=”upload.php”> <input type=”file” name=”plik” /> <input type=”hidden” name=”MAX_FILE_SIZE” value=”512000″ /> <input type=”submit” value=”Dodaj plik” /> </form> |
Umieszczanie plików na serwerze za pośrednictwem skryptu niesie ze sobą wiele niespodzianek. Począwszy od obciążania serwera dużymi plikami, po wymyślne ataki na źle skonfigurowany system. My napiszemy bezpieczny skrypt, który posłuży nam do wrzucania na serwer małych obrazków. Jak wspomniane to było wcześniej tablica superglobalna przechowująca w sobie zmienne dostarczane, podczas przesyłania plików nosi nazwę $_FILES. $_FILES jest tablicą wielowymiarową tzn. że teoretycznie możemy uploadować kilka plików. Do tablicy zawierającej właściwości naszego pliku możemy odwołać w następujący sposób $_FILES[nazwa_pola_formularza]. PHP udostępnia nam takie oto właściwości:
- $_FILES[plik][name] – zwraca nazwę pliku po stronie klienta.
- $_FILES[plik][type] – zwraca MIME TYPE, czyli typ danych pliku.
- $_FILES[plik][tmp_name] – zwraca tymczasową ścieżkę zapisanego pliku.
- $_FILES[plik][error] – zwraca kod błędu, jeśli wystąpią.
Typy błędów
- 0 – plik został uploadowany pomyślnie!
- 1 – plik posiadana większą wagę niż określono w pliku php.ini
- 2 – plik posiada większą wagę niż określono to w formularzu w polu MAX_FILE_SIZE
- 3 – plik nie został przesłany w całości
- 4 – nie było żadnego pliku do uploadowania
Na początku naszego skryptu musimy sprawdzić czy w ogóle jakiś plik został uploadowany, służy do tego funkcja is_uploaded_file(), która pobiera jako argument ścieżkę tymczasową do pliku i zwraca odpowiednio true, gdy plik wysłano oraz false gdy pliku nie wysyłano. Następnie za pomocą instrukcji warunkowej if oraz tablicy $_FILES[plik] należy sprawdzić czy nie zostały wywołane błędy podczas przesyłania pliku. Następnym krokiem jest sprawdzenie MIME TYPE (przecież nie pozwolimy, by ktoś wysłał nam pliczek PHP i później go wykonał 🙂 ja bym nie pozwolił). Wysyłamy obrazki zatem ustalamy, że dopuszczalne MIME TYPE to image/png, image/pjpeg oraz image/gif. Po tych żmudnych sprawdzaniach możemy wreszcie skopiować nasz pliczek służy do tego funkcja move_uploaded_file(), która jako pierwszy argument pobiera źródło, natomiast jako drugi przeznaczenie. Zwraca ona true w przypadku pomyślnego skopiowania pliku i false w przeciwnym wypadku. Bardzo ciekawy jest fakt, iż więcej linijek odpowiada za sprawdzanie niż za same wysyłanie pliku. Przedstawiliśmy jak ma wyglądać nasz skrypt czas przejść do jego napisania:
<?php //Sprawdzamy czy plik został wysłany if(!is_uploaded_file($_FILES[plik][tmp_name])){ echo „Musisz wybrać jakiś plik Cwaniaczku! „; exit;} //Sprawdzamy czy nie ma błędów if($_FILES[plik][error] != 0){ echo „Wystąpił błąd podczas przesyłania pliku! Sprawdź czy plik posiada odpowiednią wagę i spróbuj ponownie! „; exit;} //Sprawdzamy MIME TYPE pliku if($_FILES[plik][type] == „image/pjpeg” || $_FILES[plik][type] == „image/png” || $_FILES[plik][type] == „image/gif”){ //Dotarliśmy tutaj to wszystko jest OK = UPLOAD if(move_uploaded_file($_FILES[plik][tmp_name], „./upload/”.$_FILES[plik][name])){ echo „Plik wrzucono pomyślnie! „; exit; }else{ echo „Wystąpił nieoczekiwany błąd podczas kopiowania! „; exit; } }else{ echo „Możesz uploadować tylko obrazki! „; exit; } ?> |
Blokowanie niektórych funkcji
W pliku php.ini istnieje dyrektywa pozwalająca na zablokowanie niektórych funkcji jak np. phpinfo(), system() czy fopen(). Funkcje, które chcemy zablokować należy oddzielić przecinkami po dyrektywie disable_functions np.
disable_functions = phpinfo, system, fopen, fsockopen |
Powyższy przykład blokuje funkcje: phpinfo(), system(), fopen() oraz fsockopen(), gdy spróbujemy użyć tych funkcji pokaże nam się błąd podobny do poniższego:
Warning: phpinfo() has been disabled for security reasons in c:\usr\krasnal\www\php\server.php on line 4 |
Dlaczego zablokowaliśmy właśnie te funkcje? Nie wszyscy administratorzy serwerów chcą ujawniać informację o swoim systemie. Funkcja phpinfo() udostępnia wiele pożytecznych dla nas danych, ale te dane w złych rękach mogą posłużyć za klucz do drzwi naszego systemu. Dlaczego fsockopen()? A no dlatego, ażeby zwiększyć wydajność naszych skryptów i nie być posądzonym o spam w Internecie. Funkcja ta posiada niewyobrażalne możliwości, których nie będę tu opisywał.
Zasady pisania bezpiecznych aplikacji PHP
Pierwsza i najważniejsza zasada czyli wyłączone register_globals, jeśli nie możemy tego zrobić to należy każdej zmiennej warunkowej ustawić wartość domyślną na początku skryptu. Następnie należy pamiętać o bezpieczeństwie includowania plików, czyli przede wszystkim sprawdzanie i filtracja danych wejściowych wprowadzonych przez użytkownika. Teraz coś nowego, często na serwerze widzimy pliki o rozszerzeniu inc (od include), nie są to skrypty PHP, lecz zawierają kod, często dane do bazy MySQL (np. config.inc), gdy wywołamy z adresu na niezabezpieczonym serwerze skrypt config.inc ujrzymy hasło do bazy i nieszczęście gotowe. Nie powinniśmy zapisywać ważnych informacji w plikach tekstowych i plików inc (jeśli nie są one interpretowane jako skrypty PHP). Jeśli mamy możliwość należy pracować w trybie safe_mode oraz mieć poprawnie skonfigurowaną dyrektywę open_basedir. Ustawiane przez nas prawa dostępu CHMOD nie powinny być nadawane na wyrost! CHMOD 644 powinien wystarczyć dla pliku/skryptu oraz CHMOD 755 dla katalogu. Jednak cały nacisk powinien być położony na filtrację danych użytkownika zarówno wysyłanych metodami POST i GET jak i umieszczanych w bazie i zmiennych sesyjnych. Kolejna rzecz: sprawdzajmy czy skrypt został wywołany z naszej strony za pomocą zmiennej $_SERVER[HTTP_REFERER] lub sprawdzanie identyfikatora sesji session_id();. Gdyż skrypt metodą POST możemy wykonać za pomocą funkcji komunikacji sieciowej – cURL (odsyłam do artykułu mojego autorstwa zamieszczonego w serwisie Webinside.pl).Rejestruj datę i ip wywołania skryptu! UWAGA! nie ró tego w formularzu przesyłającym dane, gdyż można to ominąć. Kolejna i chyba bardzo ważna rzecz: przeglądaj logi serwera WWW – wiele ciekawych rzeczy można się z tego dowiedzieć.
Podsumowanie
To by było chyba na tyle. Opisane wyżej metody nie zabepieczają całkowicie naszych skryptów, ale napewno utrudniają ich kontrolowanie przez innych.
Jeżeli masz jakieś pytania zadaj je na naszym forum
UWAGA – artykuł umieszczony w celach edukacyjnych! Autor nie ponosi odpowiedzialności za złe wykorzystanie przedstawionej wiedzy!