Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Hardening aplikacji PHP z wykorzystaniem OWASP PHP ESAPI i PHPIDS
Wstęp
Rosnąca z roku na rok liczba ataków na aplikacje webowe bezlitośnie obnaża podejście programistów do kwestii związanych z zabezpieczeniami. Mimo powszechnej świadomości o zagrożeniach takich, jak: SQL injection, XSS (Cross-Site Scripting), Remote Code Execution czy Local/Remote File Include, wciąż możemy natknąć się na aplikacje webowe, gdzie bez większych trudności można takie błędy znaleźć i co gorsza, wykorzystać np. w celu kradzieży danych czy uzyskania nieuprawnionego dostępu do samej aplikacji bądź serwera, na którym ona działa.
Pomimo działań organizacji takich, jak OWASP czy WASC, przygotowywane przez nie raporty nie pozostawiają złudzeń – większość serwisów, które codziennie odwiedzamy, jest narażona na co najmniej jedną z wymienionych podatności.
W tekście przyjrzymy się, w jaki sposób użyć: OWASP ESAPI oraz PHPIDS do zabezpieczenia własnej aplikacji napisanej w języku PHP.
ESAPI – podstawy
Projekt jest rozwijany jako open source i niestety, jak to z wieloma tego typu projektami bywa, nieco zaniedbany. Jak w przypadku wielu innych projektów, których „efektowność” dla końcowego klienta jest niezauważalna – ESAPI nie jest biblioteką powszechnie znaną. A szkoda, bo dość dobrze sprawdza się w swojej podstawowej roli i przede wszystkim – zdejmuje z barków programisty ciężar zbudowania uniwersalnego i łatwego w implementacji mechanizmu, który można zastosować zarówno w lokalnej aplikacji, z której korzysta tylko kilka osób, jak i w dużym portalu odwiedzanym dziennie przez setki czy tysiące użytkowników.
Lista firm, które używają bądź używały ESAPI w produkcyjnych projektach, zawiera choćby takie marki jak: American Express, Apache Foundation, Lockheed Martin, SANS Institute, United States Navy czy Mitre. Opiekunem projektu od dłuższego czasu jest firma Aspect Security, zajmująca się oprogramowaniem między innymi do analizy kodu źródłowego w czasie rzeczywistym pod kątem bezpieczeństwa (Contrast Security). Choć wsparcie komercyjne projektu cieszy, to jednak ze względu na profil działalności Aspect Secrity skupia się na wersji ESAPI dla języka Java.
Szczegóły implementacyjne oraz sposób działania PHP ESAPI, czyli wersji biblioteki dla języka PHP, dość dokładnie opisał Mike Boberski, Project Manager projektu. Ten kilkustronicowy PDF wyjaśnia kilka podstawowych wzorców implementowania biblioteki we własnym kodzie.
PHPIDS – podstawy
PHPIDS działa wykorzystaniem zdefiniowanych reguł, co pozwala w łatwy sposób reguły te modyfikować albo dodawać nowe (np. specyficzne dla naszej aplikacji). W dalszej części artykułu przyjrzymy się krok po kroku, jak dodać PHPIDS do przykładowej aplikacji.
Przyjrzyjmy się przykładowej regule (w formacie JSON), by zorientować się, na co możemy liczyć w chwili zaprzęgnięcia do pracy systemu wykrywania włamań w postaci omawianej biblioteki:
{
"id":"33",
"rule":"(?:\\<\\w*:?\\s(?:[^\\>]*)t(?!rong))|(?:\\^lt;scri)|(<\\w+:\\w+)",
"description":"Detects obfuscated script tags and XML wrapped HTML",
"tags":{
"tag":"xss"
},
"impact":"4"
},
Sama reguła dla interpretera zaimplementowanego w bibliotece jest wyrażeniem regularnym. Oprócz reguły wpis zawiera także komunikat, który zostanie zapisany w logach w przypadku wykrycia opisanego ataku przez PHPIDS, tag określający, do jakiej klasy ataków należy filtr (w tym przypadku jest to XSS) oraz wskazówkę, jak groźny dla aplikacji może być udany atak wykorzystujący ten wektor (pole „impact”).
Bardzo bogatym źródłem informacji na temat implementacji czy używania biblioteki we własnych projektach można znaleźć na oficjalnym forum pod adresem The english speaking community from PHPIDS.
Uruchamiamy aplikację testową
W celach demonstracyjnych posłużymy się niewielką aplikacją testową. Nie będzie to nic szczególnego – prosta baza danych z typową funkcjonalnością określaną akronimem CRUD (Create – utwórz, Read – odczytaj, Update – zmodyfikuj, Delete – usuń).
Wszystkie operacje wchodzące w skład aplikacji będą prostymi modyfikacjami bazy danych (dodawanie, modyfikacja, odczyt i usuwanie rekordów).
Do uruchomienia testowej aplikacji konieczne będzie posiadanie serwera bazodanowego MySQL oraz dowolnego serwera WWW (najwygodniej będzie użyć serwera wbudowanego w interpreter PHP od wersji 5.4). Poniższa instrukcja zakłada wykorzystanie właśnie serwera wbudowanego w interpreter języka PHP.
Gotową aplikację testową można pobrać z repozytorium Git. W tym celu należy wykonać w konsoli polecenie:
$ git clone git@github.com:bl4de/hardening-php-apps-with-phpids-and-phpesapi.git
bądź posłużyć się dowolnym klientem systemu kontroli wersji Git.
Następnie na swoim serwerze MySQL tworzymy bazę danych o nazwie 'sampleapp’ i wykonujemy skrypt SQL o nazwie sampleapp-schema.sql (znajduje się go w katalogu z pobranym repozytorium), który utworzy naszą testową tabelkę i wypełni ją danymi. W pliku bootstrap.php podajemy odpowiednie dane autoryzacyjne (zmienne o nazwach $db_username oraz $db_password).
Ostatnim krokiem będzie wykonanie sekwencji poleceń (w katalogu z kodem naszej aplikacji):
$ git checkout master $ php -S localhost:3000
Po otworzeniu w przeglądarce adresu http://localhost:3000 powinien pojawić się ekran powitalny naszej testowej aplikacji.
Ta aplikacja jest w tej chwili całkowicie bezbronna. Kilka prostych testów pozwala bardzo szybko zorientować się, że jest podatna choćby na atak SQL Injection. Proste wstrzyknięcie w banalny sposób ujawnia nam informacje o nazwie użytkownika oraz wersji serwera MySQL:
http://localhost:3000/?p=details&id=2 and 1=2
union select null,version(),user()–
Oczywiście to niedopuszczalne, by nieprzefiltrowane dane z żądań HTTP trafiały bezpośrednio jako parametry do zapytań SQL. Wydaje się to oczywiste, jednak praktyka pokazuje, że wielu programistów o tym nie pamięta albo nie potrafi zabezpieczyć się przed tego rodzaju zagrożeniem.
Spróbujmy zatem doprowadzić naszą aplikację do stanu, gdy ewentualne próby ataków będą:
- zablokowane przez bibliotekę, która zadba o odfiltrowanie niebezpiecznych danych przesyłanych w żądaniach HTTP,
- zapisane do plików logów oraz zgłoszone administratorom serwisu, by mogli oni podjąć odpowiednie czynności w celu zablokowania ataku oraz zgłoszenia próby jego wykonania.
Oczywiście rozwiązań tak postawionego problemu jest co najmniej kilka – możemy użyć np. dodatkowej warstwy abstrakcji dostępu do bazy danych, wykorzystać zapytania parametryzowane czy użyć systemu ORM (ORM – Object-Relational Mapping).
Można także skorzystać z bibliotek wyspecjalizowanych tylko i wyłącznie w celu zapewnienia naszej aplikacji pełnego bezpieczeństwa. My użyjemy do tego opisanych we wstępie bibliotek OWASP PHP ESAPI i PHPIDS
Pierwsza linia obrony – OWASP PHP ESAPI
Rozpoczniemy zabezpieczenie naszej aplikacji od realizacji pierwszego założenia: zaimplementujemy bibliotekę PHP ESAPI, która posłuży nam do walidacji danych wejściowych.
Aby przełączyć się na wersję testową aplikacji z zaimplementowaną biblioteką ESAPI, w konsoli należy wykonać następujące polecenie:
$ git checkout esapi
a następnie odświeżyć stronę w przeglądarce.
Dołączenie ESAPI do aplikacji polega na dopisaniu odpowiednich instrukcji include bądź require w miejscu, w którym dołączamy wszystkie zewnętrzne zasoby – w naszym przypadku jest to plik bootstrap.php.
// boostrap.php
require_once 'ESAPI/src/reference/DefaultValidator.php';
require_once 'sampleapp.validator.php';
require_once 'sampleapp.application.php';
W celu stworzenia walidatora danych wejściowych dla naszej aplikacji posłużymy się gotowymi klasami, dostępnymi w katalogu src/references, implementującymi odpowiednie interfejsy biblioteki. To pozwoli nam otrzymać gotowe do użycia obiekty ściśle dopasowane do naszych wymagań.
Pierwszą klasą, której użyjemy, będzie domyślna klasa walidatora danych wejściowych biblioteki ESAPI (src/reference/DefaultValidator.php). Na jej bazie utworzymy naszą własną, małą implementację zawierającą kilka użytecznych metod:
// sampleapp.validator.php class SampleappValidator extends DefaultValidator { public function __construct() { parent::__construct(); } }
Dla osób niezorientowanych w zagadnieniach związanych z dziedziczeniem oraz innymi specyficznymi dla programowania zorientowanego obiektowo terminami polecam zapoznać się z wprowadzeniem do OOP, a także z dokumentacją języka PHP i tematów związanych z programowaniem obiektowym w tym języku.
Implementacja ESAPI w naszej aplikacji wymaga kilku modyfikacji w konstruktorze klasy Application. Dodaliśmy do niej kilka nowych właściwości prywatnych ($ESAPI, $encoder oraz $validator), a następnie zainicjalizowaliśmy je w sposób zaprezentowany na listingu:
(...) private $ESAPI; private $validator; private $encoder; (...) public function __construct() { (...) $this->ESAPI = new ESAPI('esapi/ESAPI.xml'); ESAPI::setValidator(new SampleappValidator()); ESAPI::setEncoder(new DefaultEncoder()); $this->validator = ESAPI::getValidator(); $this->encoder = ESAPI::getEncoder(); (...) }
Warto zwrócić uwagę na argument, który przekazaliśmy w konstruktorze obiektu klasy ESAPI. Jest to ścieżka do pliku ESAPI.xml będącego kompleksową konfiguracją biblioteki. Nie będziemy wprowadzać do niego żadnych modyfikacji, jednak polecam zapoznać się z zawartością tego pliku – pozwoli to na zorientowanie się w sporych możliwościach biblioteki i udostępnianych przez nią klasach.
Od tego momentu możemy posługiwać się w metodach klasy Application walidatorem SampleappValidator. Spróbujmy zatem zaimplementować logikę, która zabezpieczy nas przed atakiem SQL Injection zaprezentowanym wcześniej.
Zidentyfikujmy wektor ataku – jest nim parametr id z adresu url strony prezentującej nam szczegóły rekordu ((http://localhost:3000/?p=details&id=2
).
Kolejną czynnością będzie odnalezienie fragmentu kodu, który służy do budowania zapytania do bazy z odczytanym id. Znajduje się on w pliku details.inc.php:
// details.inc.php (...) $_id = (isset($_GET['id']) && (int)$_GET['id'] > 0) ? $_GET['id'] : null; [1] $d = $this->getRows($this->execute('SELECT * FROM entry WHERE id = ' . $_id)); $details = $d[0]; (...)
Doświadczonemu programiście od razu rzuca się w oczy brak jakiejkolwiek walidacji danych pochodzących z żądania GET ([1]), choć na pierwszy rzut oka wydaje się, że sprawdzenie warunku (int)$_GET[’id’] > 0 jest taką właśnie walidacją, prawda? Otóż nie do końca.
Problem dla niedoświadczonego programisty może być dość subtelny, ale rzut oka do dokumentacji języka wszystko wyjaśnia. Operacja rzutowania na typ liczbowy całkowity (int) nie modyfikuje wartości zmiennej (chyba, że przypiszemy wynik takiej operacji do nowej zmiennej). Gdy rzutujemy dowolny ciąg zaczynający się od prawidłowej liczby – reszta ciągu jest po prostu odrzucana (PHP.net – String conversion to numbers).
Dlatego sprawdzenie warunku (int)’3 and 1=2 union select null, version(), database()–’ > 0 zwróci true, gdyż zgodnie z powyższym, końcowym porównaniem będzie 3 > 0, co jest logiczną prawdą.
Następnie parametr id jest podstawiany do zapytania SQL dokładnie w takiej postaci, w jakiej został przesłany z przeglądarki – i mamy gotową podatność na wstrzyknięcie SQL poprzez parametr id.
Dla potrzeb naszej aplikacji wiemy, że omawiany parametr to identyfikator rekordu w tabeli entry. Powinien być liczbą całkowitą, większą od zera. Każda inna wartość jest nieprawidłowa i powinna być odrzucana, a o przekazaniu do zapytania SQL nie ma już w tym momencie mowy.
Zaimplementujmy zatem taką walidację, korzystając z dostępnego w naszej klasie Application walidatora, zamieniając linijki wskazane w listingu powyżej na następujące:
// details.inc.php (...) if (isset($_GET['id']) && $this->validator->isValidNumber("ID", $this->encoder->canonicalize($_GET['id']), 1) ) { $_id = (int)$_GET['id']; $d = $this->getRows($this->execute('SELECT * FROM entry WHERE id = ' . $_id)); $details = $d[0]; } else { $msg = "id is not a valid integer value!"; } (...)
Przyjrzyjmy się dwóm instrukcjom. Pierwsza z nich, to wywołanie metody isValidNumber() walidatora – sprawdza ona, czy odczytany z url parametr jest faktycznie wartością liczbową oraz czy jego minimalna wartość wynosi 1 (trzeci argument). Pierwszy argument wywołania metody (ciąg „ID”) to tzw. kontekst, będący opisem, czego walidacja dotyczy.
Z kolei wywołanie metody canonicalize() „upraszcza” wartość zmiennej do najbardziej podstawowej wartości, dającej się zwalidować (np. „ucina” ciągi znaków dodane do wartości liczbowych).
Rzutowanie parametru id odczytanego z $_GET na typ liczbowy wewnątrz warunku to już tylko formalność – warunek i tak nie wykonałby się, gdyby id nie przeszło naszej walidacji.
Mały test pozwoli nam stwierdzić, że wykorzystanie tego wektora ataku do wykonania SQL Injection nie jest już możliwe:
Spróbujmy dodać nasz własny walidator. Załóżmy, że aplikacja umożliwia nam tworzenie listy krajów wraz z odpowiadającymi im tzw. kodami ISO. Kody ISO to dwuliterowe identyfikatory, używane np. w domenach internetowych, identyfikujące kraj pochodzenia. Dla przykładu kod ISO Polski to PL, Stanów Zjednoczonych Ameryki Północnej to US, Niemiec – DE, Francji – FR.
Ponieważ mamy utworzoną klasę SampleappValidator, tam zaimplementujemy nasz kod, wzorując się na innych metodach z rdzennej klasy DefaultValidator biblioteki ESAPI:
// sampelapp.validator.php class SampleappValidator extends DefaultValidator { public function __construct() { parent::__construct(); } private function _assertValidIsoCode($context, $input, $maxLength, $minLength) { $isocode = new StringValidationRule('StringValidator', $this->_encoder, $format); $isocode->setMinimumLength($minLength); $isocode->setMaximumLength($maxLength); $isocode->assertValid($context, $input); return null; } /** * @inheritdoc */ public function isValidIsoCode($context, $input, $maxLength, $minLength{ try { $this->_assertValidIsoCode($context, $input, $maxLength, $minLength); } catch (Exception $e) { return false; } return true; } }
Wykorzystanie naszej nowej walidacji w formularzu dodawania rekordów jest łatwe – sprowadza się do wywołania metody isValidIsoCode z właściwymi argumentami:
// add.inc.php (...) if (!empty($_POST)) { if ($this->validator->isValidIsoCode("ISO code", $_POST['value'], 2, 2) ) { $value = strtoupper($_POST['value']); } else { $value = "??"; } $q = 'INSERT INTO entry (name, value) VALUES ("' . $name . '", "' . $value . '")'; $res = $this->execute($q); if (@mysql_insert_id($this->db) > 0) { header('Location: ?p=list'); } else { $error = "Add new record failed."; } } (...)
W tym momencie w polu przeznaczonym na kod ISO nie jest możliwe wprowadzenie innej wartości, niż dwuznakowego ciągu (dla ułatwienia ciąg ten zamieniany jest na duże litery, zanim trafi do zapytania SQL). W przypadku, gdy nasza walidacja zwróci false, do bazy zapisywane są dwa znaki zapytania. Analogiczną walidację powinniśmy również zastosować w formularzu edycji.
To tylko niewielki wycinek możliwości, jakie daje biblioteka ESAPI. Przy naprawdę rozbudowanych aplikacjach zaprezentowane podejście pozwala nam w bardzo prosty i przejrzysty sposób skupić całą walidację danych wejściowych w jednym miejscu. Możemy dodawać kolejne, specyficzne walidatory i mieć do nich dostęp w całym zakresie naszej aplikacji. Zawsze mamy pewność, że walidacja działa jednakowo, niezależnie od tego, gdzie i dla jakiego argumentu zostanie wywołana. Podejście takie ułatwia także testowanie aplikacji.
Druga linia obrony – PHPIDS
Jak do tej pory, zrealizowaliśmy pierwsze założenie, czyli zaimplementowaliśmy mechanizm walidujący dane wejściowe i zabezpieczający naszą aplikację przed atakami. Pora zabrać się za drugi punkt – wychwytywanie i raportowanie wszelkich prób ataków przeprowadzanych w celu złamania zastosowanych zabezpieczeń.
Do realizacji tego celu posłuży nam opisywana we wstępie biblioteka PHPIDS.
Aby uruchomić wersję testowej aplikacji z zaimplementowanym systemem wykrywania włamań, w konsoli wykonujemy polecenie:
$ git checkout phpids
a następnie odświeżamy stronę w przeglądarce.
Dodanie obsługi systemu IDS do naszej aplikacji wymaga kilku modyfikacji pliku „startowego” (bootstrap.php):
// bootstrap.php require_once 'ESAPI/src/reference/DefaultValidator.php'; require_once 'sampleapp.validator.php'; require_once 'sampleapp.application.php'; // import klas PHPIDS require_once 'IDS/Init.php'; require_once 'IDS/Monitor.php'; require_once 'IDS/Filter/Storage.php'; require_once 'IDS/Filter.php'; require_once 'IDS/Log/File.php'; require_once 'IDS/Log/Composite.php'; require_once 'IDS/Report.php'; require_once 'IDS/Event.php'; require_once 'IDS/Converter.php'; require_once 'IDS/Caching/CacheFactory.php'; require_once 'IDS/Caching/CacheInterface.php';
Dodamy teraz obsługę PHPIDS w naszej aplikacji, czyli klasie Application. Chcemy, by IDS przechwytywał wszystkie zdefiniowane w regułach próby ataków przy każdym żądaniu HTTP, więc nasz kod umieścimy w konstruktorze. Instancję PHPIDS oraz jego konfigurację będziemy przechowywać w prywatnych składowych naszej klasy (o nazwie $ids oraz $idsConfig) tak, by mieć do niej dostęp we wszystkich pozostałych metodach.
To, co znajduje się w pliku przekazywanym jako argument do statycznej metody IDS\Init::init( 'IDS/Config/Config.ini.php’ ) to konfiguracja naszego IDSa – wrócimy do tego pliku za chwilę. Najpierw przekonajmy się, czy nasz system wykrywania włamań zadziała.
// sampleapp.application.php (...) private $ids; private $idsConfig; public function __construct() { $this->db = @mysql_connect( 'localhost', $this->db_username, $this->db_password ); if ( is_resource( $this->db ) ) { mysql_select_db( 'sampleapp' ); } else { throw new Exception( "Database connection error, could not connect to database :/" ); } // ESAPI $this->ESAPI = new ESAPI( 'esapi/ESAPI.xml' ); ESAPI::setValidator( new SampleappValidator() ); ESAPI::setEncoder( new DefaultEncoder() ); $this->validator = ESAPI::getValidator(); $this->encoder = ESAPI::getEncoder(); // PHPIDS $request = array( 'GET' => $_GET, 'POST' => $_POST, 'COOKIE' => $_COOKIE ); $this->idsConfig = IDS\Init::init( 'IDS/Config/Config.ini.php' ); $this->ids = new IDS\Monitor( $this->idsConfig ); $this->runIds( $request ); [1] } private function runIds($request) { $res = $this->ids->run($request); if (!$res->isEmpty()) { echo $res; } } (...)
Prywatna metoda runIds(), wywoływana w [1], uruchamia cały engine biblioteki. Jako argument wywołania przekazujemy jej tablicę z informacją, które zmienne globalne mają być przez IDS sprawdzane (w naszym przypadku będą to dane pochodzące z żądań HTTP GET i POST oraz tablica COOKIE przechowująca ciasteczka). Aby przekonać się, czy PHPIDS działa zgodnie z założeniami, wykonamy dwie proste próby ataków (SQL Injection oraz klasycznego wstrzyknięcia XSS z tagami <script>):
http://localhost:3000/?p=details&id=3%20and%201=2%20union%20select%201,2,version()–
Naszym oczom powinien ukazać się log, wskazujący na miejsce, w którym system wykrywania włamań dopatrzył się próby ataku:
Przeanalizujmy informację, jaką zaserwował nam IDS. W pierwszej linijce znajduje się informacja o tym, który parametr żądania zawierał wstrzyknięcie złośliwego kodu i jak to wstrzyknięcie wyglądało:
Variable: GET.id | Value: 3 and 1=2 union select 1,2,version()–
Następnie widzimy informację o potencjalnej „sile” ataku, będącej sumą parametrów impact ze wszystkich naruszonych reguł (o regułach PHPIDS możesz przeczytać na początku opracowania).
Kolejne linijki to opisy poszczególnych naruszonych reguł. Możemy się z nich dowiedzieć między innymi, że zostały wykryte znaki komentarzy SQL czy próby „wyciągnięcia” nieautoryzowanych informacji z serwera bazodanowego, a także kluczową informację – o próbie dołączenia własnego zapytania SQL (Detects concatenated basic SQL injection).
Podobny test przeprowadźmy dla próby ataku XSS (Cross Site Scripting):
http://localhost:3000/?p=details&id=3%3Cscript%3Ealert(%27xss!%27)%3C/script%3E
Także w tym przypadku reakcja IDS była bezbłędna, włącznie z rozpoznaniem, że nasza próba ataku należała do kategorii „trywialnych” (Detects very basic XSS probings ;) ) Co ciekawe, IDS potraktował ten atak również jako próbę przeprowadzenia ataku SQL Injection (spowodowane zostało to obecnością ciągu znaków doklejonego do parametru id).
W produkcyjnej aplikacji oczywiście niedopuszczalne jest, by tego rodzaju komunikaty pojawiały się bezpośrednio w oknie przeglądarki. W tym celu musimy tak skonfigurować nasz IDS, by informacje o potencjalnych próbach ataku były zapisywane w pliku dziennika bądź w bazie danych, a administrator bądź programista otrzymywał powiadomienia mailowe.
W tym celu musimy dodać do pliku Config.ini.php sekcję [Logging] i odpowiednio ją skonfigurować:
[Logging] ; file logging path = /var/www/web1/phpids/lib/IDS/tmp/phpids_log.txt ; email logging ; note that enabling safemode you can prevent spam attempts, ; see documentation recipients[] = test@test.com.invalid subject = "PHPIDS detected an intrusion attempt!" header = "From: <PHPIDS> info@php-ids.org" safemode = true allowed_rate = 15 ; database logging wrapper = "mysql:host=localhost;port=3306;dbname=phpids" user = phpids_user password = 123456 table = intrusions
Powyżej przykładowa konfiguracja logowania zdarzeń dla PHPIDS – sekcja [Logging] pliku Config.ini.php.
Korzystając z powyższego wzoru, dodajmy proste logowanie zdarzeń do pliku dziennika. W tym celu dopiszmy w pliku konfiguracyjnym odpowiednią linijkę, zawierającą ścieżkę do niego:
// Config.ini.php (...) [Logging] ; file logging path = tmp/phpids.log (...)
Drugim krokiem będzie zmodyfikowanie naszej metody runIds() tak, by w przypadku wykrycia zagrożenia odpowiednie informacje trafiły do logów:
// sampleapp.application.php (...) private function runIds( $request ) { $res = $this->ids->run( $request ); if ( !$res->isEmpty() ) { // echo $res; $log = new IDS_Log_Composite(); $log->addLogger( IDS_Log_File::getInstance( $this->idsConfig ) ); $log->execute( $res ); } } (...)
Teraz, jeśli powtórzymy któryś z naszych ataków, w oknie przeglądarki nie zauważymy nic szczególnego, co wzbudziłoby nasze podejrzenia (w przypadku, gdy bylibyśmy crackerem usiłującym złamać zabezpieczenia naszej aplikacji), jednak w pliku phpids.log pojawił się zapis dokumentujący naszą próbę.
Podsumowanie
Oczywiście przedstawione powyżej podstawy zastosowania bibliotek PHP ESAPI oraz PHPIDS to wierzchołek góry lodowej, którą jest tematyka związana z bezpieczeństwem współczesnych aplikacji internetowych. Jednak pozwoliły nam one zapoznać się z dwiema fundamentalnymi zasadami, o których należy pamiętać, gdy budujemy aplikację webową:
- zawsze należy walidować dane wejściowe pochodzące z dowolnego źródła – niezależnie od tego, czy są to dane wprowadzane przez użytkownika, pochodzące z zewnętrznych usług sieciowych czy też z innych zasobów,
- logujemy wszelkie odbiegające od normy zachowania aplikacji, żądania HTTP, komunikację sieciową – w przypadku próby ataku będziemy mogli przeanalizować, w jaki sposób do niego doszło i odpowiednio zareagować.
Źródła
PHPIDS
- oficjalna strona projektu PHP IDS (UWAGA! strona przy wejściu informuje o wygaśnięciu certyfikatu SSL!)
- oficjalne repozytorium PHPIDS w serwisie GitHub | GitHub
- Intrusion Detection For PHP Applications With PHPIDS | HowToForge
- Defending Web Applications with PHPIDS | Mad Irish . net
OWASP PHP ESAPI
- OWASP Enterprise Security API | OWASP
- repoytorium kodu źródłowego owasp-esapi-php | Google Code
- Using the OWASP PHP ESAPI – Part 1 | Jackwillk Security
- Using the OWASP PHP ESAPI – Part 2 | Jackwillk Security
- Using the OWASP PHP ESAPI – Part 3 | Jackwillk Security
- Using the OWASP PHP ESAPI – Part 4 | Jackwillk Security
- A New Open Source Tool: OWASP ESAPI for PHP | pdf, rozmiar: 262 kB
Rafał 'bl4de’ Janicki – bloorq[at]gmail.com
A może by tak się bronić przed wszystkim złem tego świata już na poziomie serwera www, jakimś modułem?
@Michał, oczywiście, że można. Prezentowane biblioteki zapewniają Ci ochronę i monitorowanie jedynie na poziomie aplikacji. Ma to tę zaletę, że jeśli przeniesiesz aplikację np. na inny serwer (nginx, IIS) – ta ochrona nadal pozostaje i będzie działać (a w przypadku implementowania zabezpieczeń na poziomie serwera musisz wszystko konfigurować od nowa dla każdego kolejnego serwera).
Druga sprawa – nie zawsze możesz sobie pozwolić na implementowanie własnych zabezpieczeń na poziomie serwera (np. na współdzielonym hostingu).
Trzecia rzecz – nie zaimplementujesz na poziomie serwera walidacji absolutnie wszystkich danych wejściowych. Poza tym, nie zaimplementujesz też walidacji danych otrzymywanych np. z żądań asynchronicznych do innych serwerów (zakładając, że korzystasz z CORS), danych odczytywanych z bazy danych itd. To wszystko musisz sam zabezpieczyć na poziomie aplikacji (np. korzystając właśnie z ESAPI i własnych walidatorów, tak, jak w artykule zaprezentowałem na przykładzie kodów ISO)
PHP IDS daje Ci z kolei elastyczność w postaci reguł, które możesz sobie sam definiować.
Przekonałeś mnie :)
@bl4de:
Dorzucę swoje 2 grosze, mam nieco inne doświadczenia.
WAF (weźmy na warsztat mod_security, dostępny dla Apache, nginx oraz IIS – w dwóch ostatnich jeszcze nie tak „wypasiony” jak dla Apache).
Obsługuje praktycznie każdy znany i popularny[1] sposób przesyłania parametrów w protokole HTTP, także nagłówki.
Dzięki temu, że występuje na tak szerokiej gamie serwerach WWW jest duże prawdopodobieństwo, że przechodząc z np. stosunkowo ciężkiego Apache na nginx nie musimy przepisywać konfiguracji WAF od zera.
Zasada defense in depth nie wzięła się z Marsa :) walidacja musi być na poziomie aplikacji, dodatkowa na np. warstwie dodatkowej jak ESAPI (bardziej naturalna dla aplikacji), PHPIDS (IDS wszysty w aplikację), czy WAF jest w pełni uzasadniona jeśli jest co chronić w danym systemie.
Właściwe różnice funkcjonalne między warstwą aplikacyjną, mocno z nią zintegrowaną, a WAFem to przede wszystkim możliwość stworzenia lokalnych przestrzeni identyfikatorów per user/sesja, kontrola dostępu czy logika niefunkcjonalnego bezpieczeństwa. Nie powiedziałbym w pełni prawdy zapominając o tym, że do mod_security można napisać skrypt(y) w Lua, które niemal w nieskończoność mogą rozszerzać możliwości WAFa, natomiast są to „koszty” wydajnościowe, implementacyjne często duże, dlatego warto się zastanowić gdzie zrobić to taniej, szybciej, wygodniej, itd.
Kończąc, o ile trafiliśmy na hosting współdzielony gdzie mamy niewiele więcej niż dostęp do DocumentRoota, faktycznie możemy mieć problem z dostawieniem WAFa. Natomiast pojawiają się hostingi (np. Akamai, CloudFlare – jeśli się nie mylę) gdzie już to natywnie możliwe lub sami zarządzamy serwerem WWW.
[1] – obsługuje także JSONa, gorzej będzie w przypadku użycia np. GWT (będzie problem z rozparsowaniem zserializowanych parametrów)
@Przemek
Dzięki za komentarz :)
No a zawsze można użyć podpowiedzi netbeansa aby używać wbudowanych funkcji jak http://www.php.net/manual/en/function.filter-var.php
Metod na walidację danych wejściowych jest pewnie tyle, ilu programistów ;)
Najważniejsze, żeby walidacja była skuteczna :)
skuteczna i tam gdzie potrzeba :-)
PS
Przemek z tego samego wątku to nie ja, natomiast komentarz o WAFach mój. Żeby nie mylić kto jest kim będę podpisywał się z imienia i nazwiska ;)
@Przemek Skowron
> skuteczna i tam gdzie potrzeba :-)
Trafiłeś w sedno :). Generalnie świadomość o tym, że należy walidować dane wejściowe przesyłane przez użytkownika jest dość duża wśród programistów (inna sprawa, czy i jak jest stosowana).
Natomiast już walidacja danych pochodzących z innych źródeł to rzecz w zasadzie pomijana. Chodzi o dane pochodzące np. z zapytań do bazy danych, czy odpowiedzi od zewnętrznych serwisów itp. Można się na tym ładnie przejechać, co przedstawię na realnym przykładzie.
W jednym z projektów, przy którym pracowałem, aplikacja wyświetlała użytkownikowi wiadomości, które trafiały do bazy danych z zupełnie zewnętrznego zasobu. Zapytania do bazy realizował backend, a do frontendu zwracana była lista wiadomości oraz ich szczegóły (tytuł, treść, nadawca, data wysłania itd.)
Ponieważ cała logika obsługiwana była jedynie w backendzie, nie były zaimplementowane żadne mechanizmy walidujące te dane (bo nie było możliwości „dostania” się do tych zapytań od strony frontendu, więc z założenia użytkownik nie był w stanie ich zmodyfikować). ALE istniała teoretyczna możliwość zmodyfikowania zawartości samej bazy danych (serwer bazodanowy był zupełnie odseparowany od backendu, a dane (te wiadomości dla użytkownika) zapisywał zewnętrzny system, pozostający całkowicie poza jakąkolwiek kontrolą – czyli drugi potencjalny punkt, gdzie dane mogły zostać „zepsute”.
Udało mi się przygotować PoC, który w momencie odczytywania wiadomości przez usera wykonywał atak XSS (JavaScript osadzony był w treści wiadomości). Powodem był brak walidacji po stronie backendu PRZED wysłaniem danych do użytkownika. Czysty SQL, wynik wstawiany do obiektu ArrayList, przekonwertowany na JSON i zwrócony do frontendu.
To jeszcze dodam do tego „output encoding” by dane pochodzące z niezaufanych źródeł nie były interpretowane przez interpretery (lub nie mogły wywołać interpretera) i robi się nam niemal kompletny stos zabezpieczeń jeśli chodzi o charakter ciągów znaków, które napastnik może chcieć nam podać :)
No i teraz pytanie, jaki procent aplikacji webowych ma tak kompleksowy system walidacji? Skoro przeważająca liczba z nich nie trzyma się podstawowych zasad, jak choćby testy jednostkowe (ze względu na koszty i czas od startu projektu do wdrożenia, który z reguły ma być jak najkrótszy) ?
A wiadomo, że tam, gdzie koszty grają główną rolę, priorytetem jest działająca funkcjonalność, którą można zaprezentować klientowi. Bo klient i tak nie zobaczy nigdy ani nie zrozumie, jak aplikacja działa „pod spodem”, więc często rzeczy, o których piszemy, są zaniedbywane bądź wręcz z premedytacją pomijane.
A potem zdziwienie, że „dane wyciekły”… :P
ile ma? – bazując na moim doświadczeniu: niewiele :)
natomiast z definicji to nie oznacza, że jest źle/fatalnie. analiza ryzyka powinna wykazać gdzie są granice zaufania, w jakich scenariusza trzeba, a kiedy może być zastosowana walidacja. to wynik analizy powinien podpowiedzieć czy należy znaleźć w budżecie środki na „walidację” czy nie ma takiej potrzeby. jeśli analiza mówi, że należy znaleźć, a się nie znajdzie to mamy tykającą bombę, którą ktoś zdetonuje lub będziemy mieć szczęście i nic złego się nie stanie.
oczywiście analiza ryzyka analizie nie równa, jak schrzanimy analizę lub napastnik uwzględni coś czego nie uwzględnią ci „dobrzy” to naturalnie rosną szansę, że atak zakończy się sukcesem.
jestem zwolennikiem pytań: gdzie są granice zaufania w tym systemie? na jakiego rodzaju wektory/scenariusze ataków system jest przygotowany? jakie ryzyka zostały zaakceptowane? – reakcja i odpowiedzi na te pytania pozwalają poglądowo rozpoznać sytuację, świadomość i podejście decydujących o bezpieczeństwie w firmie.