PHP a bezpieczeństwo – poradnik początkującego programisty

Dariusz Półtorak, 03-12-2010, 22:00, Komentarzy: 15

W sieci jest dużo małych, średnich i dużych witryn internetowych czy aplikacji webowych napisanych w PHP. Bardzo często widzi się komunikaty, że dana witryna została zhakowana, że wykradziono jakieś ważne dane, czy też zwykła niegroźna strona internetowa zostaje oznaczona jako dokonująca ataków, co niewątpliwie ma negatywny wpływ na jej popularność. Ten tekst ma za zadanie pokazać początkującym programistom PHP, jak należy bronić się przed złośliwcami próbującymi zniszczyć owoce naszej pracy.

PHP jest skryptowym językiem programowania powstałym w 1994 roku. Od tamtej pory zyskał znacząco na popularności i stał się jednym z wiodących języków skryptowych służących do tworzenia stron WWW. Jego możliwości na tym się nie kończą. Można swobodnie wykorzystywać go do pisania skryptów uruchamianych z wiersza poleceń, jak również do tworzenia aplikacji graficznych (PHP-GTK).

1. Nie wszystko dobre, co się na stronie pokazuje

W dobie Web 2.0 to raczej użytkownicy witryny, a nie jej właściciel wpływają na treść strony internetowej. Nawet tam, gdzie to właściciel gra główną rolę (jak np. blogi), użytkownicy dokładają coś od siebie, na przykład w postaci komentarzy. Podstawowa zasada w tym wypadku jest bardzo prosta. Wszystko, co pochodzi od użytkownika, należy, traktować jako coś złego, niebezpiecznego i gotowego w każdej chwili wybuchnąć. Uwierzcie mi, że jeżeli pojawi się szansa, by wyciąć właścicielowi witryny jakiś numer, to zawsze znajdzie się użytkownik, który go wytnie. Dla przykładu, jako treść komentarza użytkownik może zamieścić taki oto tekst:

W tym wypadku strona, na której pojawia się taki komentarz, nie będzie wyświetlana długo użytkownikowi. Zaraz po jej załadowaniu wykona się kod JavaScript, który Państwo tam widzicie, a użytkownik zostanie przekierowany na podany adres. W tym wypadku jest to Google, ale co jeżeli podana zostanie tam strona dokonująca ataków? Taka, która jest wyposażona w całą gamę exploitów mogących wykorzystać słabości naszej przeglądarki czy systemu operacyjnego? Możliwe też, że pod podanym adresem znajdzie się wierna kopia naszej witryny. Jeżeli użytkownik nie zorientuje się, że znalazł się pod inną domeną, może próbować ponownie się zalogować, myśląc, że jakiś błąd na stronie po prostu zakończył jego sesję (nastąpiło wylogowanie).

W tym momencie uzupełni formularz, przekazując użytkownikowi jego login oraz hasło. Fakt, że większość użytkowników internetu stosuje te same hasła w wielu miejscach, może sprawić, że ktoś może uzyskać dostęp do naszej skrzynki pocztowej, serwisów takich, jak Allegro czy Nasza-Klasa, i praktycznie przejąć naszą tożsamość w internecie. A tego typu oszustwa w sieci są bardzo popularne.

Jak się bronić?

W PHP istnieją dwie proste w użyciu funkcje, które nam pomogą. Są to htmlspecialchars() oraz htmlentities(). Zamieniają one znaki specjalne na encje. O ile znaki te nadal będą wyświetlane w przeglądarce użytkownika tak, jak przedtem, o tyle sama przeglądarka nie będzie takiego ciągu znaków traktować jako kodu HTML czy też JavaScript. Warto też pilnować, by w odnośnikach na stronie ktoś nie umieścił kodu JavaScript do wykonania. Na ogół po prostu trzeba przeszukać taki link pod kątem obecności ciągu znaków „javascript:”.

2. Twoje dane – moje dane

Dopuszczenie użytkownika do umieszczenia swojego kodu JavaScript bądź HTML w sposób niekontrolowany może mieć również inne skutki. Przykładowo użytkownik X jest zalogowany na swoją stronę WWW jako administrator. Wchodzi na podstronę, gdzie widnieje taki kod:

Tutaj sztuczka jest podobna do poprzedniej, jednak sama metoda ataku jest zupełnie inna. W tym wypadku przechwyciliśmy ID sesji użytkownika. Jak? Jeżeli ID sesji użytkownika przechowywany jest w ciasteczkach, JavaScript może go nam odczytać. Mnie osobiście ten kod przekierował pod taki oto adres:

Jak widzicie, mam ID sesji użytkownika. Pozostaje mi utworzyć odpowiednie ciasteczko w przeglądarce i mogę swobodnie wejść na jego konto. Dobrze przemyślany skrypt jest w stanie zrobić jeszcze więcej. Może dla przykładu przechwycić sesję użytkownika i jeżeli atakowany skrypt jest znany atakującemu, od razu zmienić hasło do konta w celu jego przechwycenia. To oczywiście najprostszy przykład, który mało gdzie zadziała, jeżeli w ogóle zadziała. Niemniej jednak brak uwagi może sprawić, że ta mała dziecina może narobić bigosu naszym użytkownikom. Niektórzy zamiast ciasteczek używają URL do przekazywania ID sesji. Ta metoda jest jeszcze bardziej niebezpieczna. Nie dość, że kod JavaScript nadal może ten ID gdzieś przesłać, to jeszcze na dodatek sam użytkownik może go przekazać, przesyłając dla przykładu link z serwisu swojemu znajomemu.

Tego typu ataki noszą miano XSS (ang. cross-site scripting).

Jak się bronić?

Po pierwsze, tak jak w poprzednim przypadku – traktujcie podejrzliwie dane przychodzące od użytkownika. Jeżeli to Wam się nie uda, to pamiętajcie, że w trakcie trwania sesji użytkownika warto śledzić jego dane. Takie rzeczy, jak user-agent przeglądarki, IP i wiele wiele innych, raczej się nie zmieniają. Kiedy użytkownik przechodzi z podstrony na podstronę, zobaczcie, co się zmienia w danych, jakie wysyła do naszego serwera (w PHP dane te można odczytać między innymi z tablicy superglobalnej $_SESSION, jeżeli zadbamy o ich przechowanie pomiędzy requestami, jak i $_SERVER, gdzie mamy podstawowe informacje o użytkowniku i naszej stronie). Jeśli wykryjemy nieprawidłowość, czyścimy superglobalną $_SESSION oraz niszczymy sesję, wywołując funkcję session_destroy();. Jest to podstawowa forma obrony, jaką możemy zastosować.

3. Zanim się wyświetli, musi się gdzieś zapisać

Zanim wyświetlimy nasze teksty bądź komentarze użytkowników na stronie WWW, musimy je gdzieś zapisać. Najczęściej takim miejscem jest baza danych. Wśród witryn napisanych w PHP największą popularnością cieszy się baza danych MySQL. Zakładam, że użytkownik zna język SQL (a przynajmniej jego podstawy). Standardowo, by wybrać nazwę jakiegoś użytkownika, stosuje się w SQL takie oto zapytanie:

Czyli po naszemu „Wybierz imię Z użytkowników GDZIE id WYNOSI 1”. Nasz użytkownik o ID 1 to Zenek. Zenek jest również właścicielem witryny i dzisiaj czeka go nieprzyjemna niespodzianka. Chcąc skonstruować kod PHP wyszukujący użytkowników po ich nazwie, skonstruował taką oto linijkę, która prowadzi wyszukiwanie:

Znalezione dane następnie są pokazywane użytkownikowi. Jakież było zdziwienie Zenka, gdy następnego dnia... nie miał już danych użytkowników. Otóż jeżeli wpiszemy jako nazwę „test” to po podstawieniu danych otrzymamy takie zapytanie:

Teraz pozostaje tylko odpowiednie zmanipulowanie ciągu znaków. Zamiast „test” wpiszemy coś takiego:

Po podstawieniu danych nasze zapytanie przyjęło taką oto postać:

Czerwona część stała się jednym zapytaniem, które nadal próbuje poszukać użytkownika o nazwie test. Jednak dalsza część została uzupełniona o zapytanie „DROP TABLE”, które nakazuje usunąć tabelę z bazy danych. Takie zapytanie skończyłoby się błędem, gdyż przy LIKE zostały nam dwa znaki %'. Dlatego na końcu wpisywanego ciągu znajduje się /*. Te dwa znaki rozpoczynają komentarz. Oznacza to tyle, że dalsza część kodu jest nieważna. Inaczej zapytanie zakończyłoby się błędem przez te pozostałości, a tego nie chcemy.

Jak widać, w tym wypadku atakujący nie musi znać zapytania SQL. Wszystko, co potrzebuje, to lista tabel w bazie danych (na co też są sposoby) i miejsce, gdzie może wykonać nasze zapytanie SQL wraz ze swoim „dodatkiem”. Tego typu atak nazywa się SQL Injection.

Jak się bronić?

Metod jest dość sporo. Pierwsza to wykorzystanie funkcji addslashes() lub mysql_escape_string() na parametrach. Metody te dodają w ciągu znaków backslashe przed znakami, które mogą wpłynąć na zapytanie SQL. W ten sposób takie znaki, jak ', zostają zamienione na \', przez co nie są interpretowane przez silnik bazy danych jako element zapytania.

Inną metodą jest przygotowanie zapytań. Na przykład w bibliotece PDO do obsługi baz danych znajdziemy metodę prepare(), której przekazujemy zapytanie SQL, wskazując miejsca, gdzie pojawią się parametry. Następnie przekazujemy tablice parametrów, a sama biblioteka zajmuje się zabezpieczaniem całości. Przykład:

Dla bardziej przezornych pozostaje jeszcze tworzenie kont do serwera bazy danych. Użytkownicy tychże mogą otrzymywać uprawnienia z dostępem do tabel oraz do wykonywania akcji na tych tabelach. Zastanówcie się, po co administratorowi możliwość usuwania tabel w bazie danych? A no w końcu jest administratorem. Ale czemu użytkownik ma mieć taką możliwość?

Dobrym nawykiem jest tworzenie kilku rodzajów kont w bazie danych. W zależności od poziomu użytkownika należy łączyć go z bazą danych z użyciem innego konta. Dzięki temu nawet gdy ktoś znajdzie sposób na zaatakowanie bazy danych, nie będzie mógł poczynić dużych szkód.

4. Są równi i równiejsi

Dobry programista zna świetnie język programowania, w którym pisze. Wielu programistów pisze w kilku językach lub je po prostu zmienia. Gdy w PHP widzi składnię podobną do tej w wielu innych językach, zakłada, że i samo zachowanie jest takie samo. Otóż nie do końca.

Nie będę tu się rozwodził nad szczegółami. Chciałbym tylko przedstawić drobną właściwość języka PHP, która na pewno wielu początkujących programistów zaskoczy. A jeżeli Was zaskoczy, to radzę wziąć się za poznawanie narzędzia, z którego korzystacie.

Otóż w PHP nie ma kontroli typów. W zmiennej może się znaleźć wszystko, od ciągu znaków, przez referencję do innej zmiennej, na obiekcie kończąc. Bardzo często różne zmienne porównujemy i tu zaczynają się dziać dziwne rzeczy...

Uruchomcie sobie taki oto kod:

Jeżeli ktoś z Was myśli, że zostanie tu wyświetlone CIAGI ZNAKOW SA ROZNE OD SIEBIE!, grubo się myli. Na ekran wyświetli nam się komunikat TE CIAGI ZNAKOW SA ROWNE!. Jeżeli takim porównaniem coś zabezpieczyliśmy, to właśnie wylądowaliśmy w niezłym kotle, nie mówiąc o tym, że wielu z Was pewnie teraz się zastanawia, jakim cudem 123 oraz 123Dariusz są sobie równe.

Jak napisałem, w PHP nie ma typów zmiennych. Niemniej jednak mechanizm porównujący zmienne musi jakoś działać. W tym wypadku sprowadza obie zmienne do jednego typu. Na nasze nieszczęście wybiera sobie typ int. Zobaczmy, co się w PHP stanie z ciągiem znaków „123Dariusz”, kiedy przeprowadzimy konwersję na typ int:

Otóż zobaczymy na ekranie 123! Konwersja zaczyna się od lewego znaku i idzie w prawo. W momencie, gdy napotkany zostanie znak, który nie pasuje do wytycznych (w tym wypadku 0-9), dalsza część zmiennej jest ucinana. Dodatkowym problemem jest zapis. Gdyby druga linia wyglądała tak:

to porównane zostałyby ze sobą dwa ciągi znaków. Jednak w naszym wypadku porównaniu uległ ciąg znaków i liczba, a obie wartości zostały sprowadzone do typu liczbowego. Może to doprowadzić do szeregu błędów i luk w naszych aplikacjach.

Jak się przed tym bronić?

Należy stosować znak identyczności ( === ) a nie porównania ( == ). Dzięki temu nie zostanie sprawdzona jedynie zawartość zmiennej, ale także jej aktualny typ. Wartości zmiennych nie będą ze sobą identyczne pod każdym względem, więc warunek nie przejdzie. Pamiętajcie tylko, że to tylko jedna z cech PHP, która może niektórych zdziwić. Jeżeli to dla Was nowość, to znaczy, że czas zagłębić się w język, by popełniać mniej błędów w przyszłości.

5. Lis w kurniku

Nie tylko komentarze dany użytkownik może umieszczać na naszej witrynie. Niejednokrotnie mogą to być pliki. Jeżeli dajecie użytkownikowi możliwość uploadu plików, zawsze upewnijcie się, co to za pliki. Jeżeli użytkownik da radę wrzucić plik PHP na Wasz serwer, może dzięki niemu wyciągnąć praktycznie każdą informację z Waszej aplikacji - od hasła do serwera bazy danych, po wszystkie dane, które się tam znajdują, na strukturze i kodzie źródłowym aplikacji kończąc. Pamiętajcie również, że kontrola typu mime czy też rozszerzenia nie wystarczy, jeżeli użytkownik ma do dyspozycji zmianę nazwy wrzuconego przez siebie pliku. A jeżeli już pozwalacie na zmianę nazwy, to pilnujcie, by użytkownik nie mógł zmienić rozszerzenia, gdyż na ogół to ono decyduje o tym, jak plik zostanie potraktowany przez serwer.

6. Gdy kota nie ma

Kolejny problem znajduje się w hostingu, którego używacie dla swoich stron WWW. Takie dane, jak zmienne sesji, są zapisywane na ogół w katalogu /tmp na serwerze (system Linux). Wielu użytkowników danego serwera ma te dane zapisywane w jednym miejscu. Chyba nie trzeba tłumaczyć, co można z taką wiedzą zrobić.

Jak się przed tym bronić?

Tutaj niestety większość zależy od administratora serwera, więc warto popytać, zanim na jakiś się zdecydujemy.

7. Przebieraniec

Kolejną wrażliwością naszej aplikacji, jaką w tym tekście omówię, będą same ciągi znaków. Okazuje się, że tekst sam w sobie może być niebezpieczny. Przefiltrowaliśmy dane wejściowe, zanim zapisaliśmy je do bazy danych, przefiltrowaliśmy przed wyświetleniem użytkownikowi, a dziwne rzeczy na naszej witrynie dalej się dzieją.

Okazuje się, że słabość naszej aplikacji może tkwić w kodowaniu znaków. Bardzo popularnym kodowaniem, jakie się obecnie stosuje, jest UTF-8. W tym kodowaniu poszczególne znaki zajmują od 1 do 3 bajtów. Obsługa tego kodowania w różnych aplikacjach jest po prostu różna. Bardzo często stosuje się mechanizmy, które starają się naprawić uszkodzony ciąg znaków, co prowadzi do pożądanych, a czasami opłakanych skutków.

Okazuje się, że nawet gdy jakaś funkcja przechwyci nieprawidłowy znak kierowany do bazy danych czy na ekran, to może go nie znaleźć, jeżeli ciąg znaków będzie „uszkodzony”. Niemniej jednak silnik bazy danych bądź przeglądarka „poprawią” ten znak i zinterpretują go jak należy. Dlatego warto pilnować tego, czy ciągi znaków na naszej stronie mają rzeczywiście takie kodowanie, jakie sobie życzymy.

W PHP istnieje kilka metod na sprawdzenie, czy UTF-8 to faktycznie UTF-8. Jedną z podstawowych jest użycie wyrażenia regularnego w takiej postaci:

Innym sposobem jest zbadanie kodowania w ten sposób:

Można też użyć rozszerzenia iconv, jeżeli mamy taką możliwość. Zaletą tego rozszerzenia jest jego wydajność. Testując iconv w PHP, przetworzyłem e-booka o wielkości prawie 30 MB w niecałą sekundę. A mało kiedy ktoś prześle nam w formularzu 30 MB tekstu.

8. Przyczajony tygrys, ukryty smok

Nieodłącznym towarzyszem każdego programisty są błędy. Dobrego programisty nie poznamy po ilości błędów, które popełnia, lecz po tym, jak sobie z nimi radzi. Pójdę w tym stwierdzeniu dalej. Jeżeli ktoś Wam kiedykolwiek powie, że jest programistą i nie popełnia błędów, to śmiało mu możecie powiedzieć, że już czas najwyższy, by programowaniem zajął się na poważnie.

W serwisach WWW błędy są bardziej niebezpieczne, gdyż ich skutki mogą dotyczyć całej rzeszy użytkowników, a nie tylko jednej osoby, której się przytrafiły. Przykładowa próba nawiązania połączenia z bazą danych wygląda tak:

Błędne dane bądź sytuacja, gdy serwer bazy danych jest niedostępny może zakończyć się takim wynikiem:

Teraz zauważcie, co się stało. Użytkownik dostał w tym momencie informacje, że plik nawiązujący połączenie jest w /home/dariuszp/public_html/, co daje mu pewien wgląd w strukturę naszego serwisu. Dostał też serwer oraz login do bazy danych. Wie również, z czego skorzystaliśmy do nawiązania połączenia, co z kolei prowadzi do informacji, jaka baza danych jest używana. To jest właśnie nasz ukryty smok. Jego działanie jest w stanie znacząco obniżyć poziom bezpieczeństwa naszej witryny.

A to jedynie wierzchołek góry lodowej. Błędy przekazują informacje o naszym serwisie. W odpowiedniej konfiguracji oprócz tej informacji możemy dostać cały backtrace skryptu. Backtrace to tablica zawierająca przebieg skryptu, czyli informacje o tym, co wydarzyło się od jego startu po nasz błąd. Istnieje nawet metoda ataku polegająca na tym, że świadomie próbuje się w serwisach wywoływać błędy (np. poprzez przesyłanie nieprawidłowych parametrów), by tego typu komunikaty otrzymać. Tak zebrane informacje mogą pomóc w przeprowadzeniu właściwego ataku.

Teraz rodzi się problem. Z jednej strony użytkownik serwisu może nam taki komunikat przekazać, co pozwoli na naprawę błędu. Z drugiej strony tenże użytkownik nie powinien nigdy tego błędu zobaczyć ze względów bezpieczeństwa. Zamiast użytkownika w końcu może czaić się tygrys gotowy rozszarpać nasz serwis na strzępy, jeżeli damy mu szansę.

Jak się przed tym bronić?

Tutaj sposobów jest kilka. Chyba najbardziej oczywistym jest przestawienie opcji display_errors oraz display_startup_errors. To jest jednak tylko częściowe rozwiązanie. Sam użytkownik zostanie troszkę zostawiony w błogiej nieświadomości z nieprawidłowo wyświetlającą się stroną. Najlepszą metodą jest napisanie własnej obsługi błędów. Wzór funkcji, która nam będzie potrzebna, wygląda tak:

Za nazwę funkcji należy podać dowolną funkcję, którą sporządzimy. Taka funkcja musi mieć co najmniej 4 parametry. Numer błędu, treść błędu, plik, gdzie błąd wystąpił, a także linia w tym pliku, gdzie błąd się pojawił. Pamiętajcie jednak, że Wasz mechanizm obsługi błędu nie zadziała w przypadku, gdy plik będzie miał błąd składni bądź sam błąd będzie dotyczył bezpośrednio ustawień serwera.

Co dalej zrobicie w swojej funkcji z błędami, zależy od Was. Możecie użytkownika przekierować na stronę główną, informując, że zaszedł jakiś zgoła niegroźny błąd (czyt. Serwis padł na amen, a my jeszcze nie wiemy, dlaczego). W międzyczasie możemy zapisać sobie informacje o błędzie do pliku wraz z całą gamą dodatkowych informacji, co pozwoli na późniejszą analizę. Odpowiednio ustawiony serwer powinien logi z informacjami o błędach gdzieś zapisywać. Jednak wielu hostingodawców tych logów łatwo nie chce oddać, więc warto mieć pod ręką własny mechanizm.

9. Podsumowanie

Pamiętajcie, że ten tekst pokrywa tylko część tematu, jakim jest bezpieczeństwo aplikacji webowych i stron internetowych. Najważniejsze, o czym programista musi pamiętać, to to, że - niezależnie od tego, czy zapisuje dane od użytkownika. czy wyświetla je na stronie - powinien zawsze sprawdzić, co te dane zawierają i odpowiednio je przefiltrować. Nie powinien pozwalać na wykonywanie złośliwego kodu. Powinien zawsze pilnować, by użytkownik wrzucający obrazek avatara wrzucił rzeczywiście obrazek na serwer. Powinien znać język programowania, którym się posługuje, by unikać sytuacji, w którym nieznajomość jego zachowań doprowadzi do powstania luki w oprogramowaniu. W międzyczasie powinien skrzętnie ukrywać swoje błędy, jednocześnie pilnując, by taki, który wystąpił, był ostatnim w tej linii kodu. Na koniec powinien zawsze sprawdzić, czy to, co dostaje od użytkownika, to to, czego się spodziewa. Mam nadzieję, że powyższy tekst przyda się początkującym programistom PHP na ich drodze do kariery i własnych projektów.

Dariusz Półtorak

Źródło: DI24.pl

Komentuj na Facebooku

Komentarze:

comments powered by Disqus
Komentarze archiwalne    Zobacz wszystkie (15)
To warto przeczytać






fot. Symetria






RSS - Wywiad
Wywiad  
RSS - Interwencje
RSS - Porady
Porady  
RSS - Listy
Listy