NFsec Logo

PHP – Bezpieczne programowanie

13/08/2009 w Bezpieczeństwo Brak komentarzy.  (artykuł nr 125, ilość słów: 1553)

P

HP jest skryptowym językiem programowania, czyli programy, które zostały w nim napisane nie są kompilowane do postaci kodu maszynowego zrozumiałego dla procesora (jak to występuje w przypadku wielu innych języków programowania), lecz wykonywane przez specjalną aplikację zwaną interpreterem PHP. Pierwotnym przeznaczeniem PHP było wspomaganie tworzenia dynamicznych stron WWW, w obecnym czasie jest on także wykorzystywany do tworzenia aplikacji dla systemów operacyjnych. Wielu początkujących programistów tego języka popełnia wiele błędów, które mają swoje odbicie w bezpieczeństwie tworzonych programów i aplikacji. Poniżej został stworzony spis niemal wszystkich błędów, najczęściej popełnianych przez programistów PHP. Poniższa lista obejmuje oraz opisuje 11 ważnych i najczęściej spotykanych luk bezpieczeństwa wraz z przykładami oraz poradami, w jaki sposób można ich uniknąć w swoich projektach.

1) SQL Injection:

Błąd tego typu pozwala na modyfikację zapytania SQL. W wyniku takiej modyfikacji możliwa jest zmiana rezultatu zapytania, bądz (nie przy MySQL, w MySQLi już tak) wykonanie dodatkowego zapytania modyfikującego rekordy, a nawet usuwającego dane z tabel.

// $_GET['id'] = "1 OR 1=1"
mysql_query('SELECT name FROM admins WHERE id = `.$_GET['id']);

Należy:

  • filtrować dane przychodzące z zewnątrz; w przypadku, gdy włączone są magic_quotes, należy wykorzystać funkcję stripslashes(), a następnie przepuścić zmienne przez natywną funkcję dla używanej bazy danych; dla MySQL jest to mysql_real_escape_string(), dla klasy DB – DB->quote().

Nie należy:

  • używać funkcji addslashes(); wbrew pozorom nie jest to funkcja, która powinna być wykorzystywana w połączeniu z bazami danych i nie ochroni naszych skryptów przed błędem tego typu.

2) Code injection:

Dość częsty błąd, również związany z brakiem walidacji pochodzących z zewnątrz, nadrzędny dla SQL Injection. Pozwala on na wykonanie dowolnego kodu PHP (w opisywanym przypadku). Zagrożenie jest duże, ponieważ w wyniku dobrze spreparowanego wywołania możliwa jest np. modyfikacja plików na serwerze czy podglądnięcie kodów źródłowych.

// $_GET['page'] = "../../../../../etc/passwd"
include `pages/`.$_GET['page'];
// $_GET['id'] = 0?;print_r(glob('*'));echo "
eval('$foo = "`.$_GET['id'].`";');

Należy:

  • bezwzględnie filtrować przychodzące dane; pamiętajcie, że niektórym danym z tablicy $_SERVER również nie można ufać (np. $_SERVER[‘PHP_SELF’]); dla powyższego przypadku skutecznym rozwiązaniem jest wykorzystanie instrukcji switch lub funkcji basename().
  • uważać podczas wykorzystania funkcji eval() w połączeniu z danymi, pochodzącymi z zewnątrz.

3) Shell Injection:

Błąd spotykany podczas wykonywania komend konsoli w połączeniu z danymi pochodzącymi od użytkownika. Jest to niezwykle groźny błąd, ponieważ umiejętnie spreparowany kod pozwala na wykonywanie komend na systemie plików, co w konsekwencji może się zakończyć usunięciem wszystkich kluczowych plików serwisu.

// $_GET['name'] = "foo | rm index.php"
passthru(`/home/user/haxor/myscript `.$_GET['name']);

Należy:

  • wykorzystać funkcję escapeshellcmd() podczas wywoływania funkcji takich jak shell_exec(), exec(), system() czy passthru(), gdy dołączamy do komendy dane pochodzące od użytkownika.
  • wykorzystać funkcję escapeshellarg() do przefiltorwania danych, które mają być wykorystane jako argument komendy konsoli.

4) Cross-Site scripting (XSS):

Dość powszechnie spotykany błąd spowodowany (znowu) brakiem filtracji danych pochodzących od użytkownika. Pozwala na dołączenie dowolnego kodu HTML. Błąd ten jest często postrzegany jako mało groźny, jednak, wbrew pozorom, może spowodować wiele problemów w połączeniu z Session Fixation. Pozwala także na przekierowanie użytkownika na inną stronę np. o identycznym wyglądzie w cely kradzieży hasła, jak również na wyciągnięcie wszystkich danych cookie.

// $_GET['name'] = "<script>alert(document.cookie);</script>"
echo `Witaj `.$_GET['name'].'!';
/*
http://example.com/index.php/%3Cscript%3Ealert(document.cookie);%3C/script%3E
http://example.com/index.php/<script>alert(document.cookie);</script>
*/
echo ‘<form method="post" action='`.$_SERVER['PHP_SELF'].'">';

Należy:

  • wykorzystać funkcje takie jak strip_tags(), htmlspecialchars() czy htmlenties()

Nie należy:

  • postrzegać tego błędu jako małe zagrożenie

5) Cross-site request forgery (CSRF):

Błąd często mylony z Cross-Site Scripting, spotykany często na wszelkiego typu forach dyskusyjnych z możliwością umieszczania plików graficznych. Pozwala na wykonanie żądania HTTP tak, jakby pochodziło od zaufanego przez system użytkownika. Wyobraźcie sobie, że jesteście zalogowani jako administrator na forum dyskusyjnym, wchodzicie na posta, w którym znajduje się następujący BBCode:

[img]http://your.forum.com/admin/delete_category.php?id=1&confirm=yes
&submit=go[/img]

Przeglądarka próbuje załadować plik graficzny, co w tym wypadku zakończy się błędem, ale nie o to chodzi. Co ważne w tym miejscu – konsekwencją tego wywołania będzie usunięcie kategorii forum.

Należy:

  • filtrować dane umieszczane przez użytkowników w postaci BBCode w poszukiwaniu adresów URL powiązanych z naszym serwisem
  • opcjonalnie wykorzystywać metodę POST dla kluczowych funkcji panelu administracyjnego
  • w ostateczności – wyłączyć BBCode w naszym serwisie

6) HTTP Response Splitting:

Błąd pojawiający się, gdy wykorzystujemy funkcję header() z nieprzefiltrowanymi danymi pochodzącymi od użytkownika. Pozwala na spreparowanie żądania HTTP w celu przekierowania na inną stronę, wyświetlenie dowolnego kodu HTML (XSS) czy utworzenie ciasteczka.

/* $_GET['id'] = "%0d%0aContent-Type:%20text/html%0d%0aHTTP/1.1%20200%20OK%0d%0a
Content-Type:%20text/html%0d%0a%0d%0a%3Cscript%20…%3C/script%3E

Wykonane żądanie:
HTTP/1.1 302 Found
Date: Wed, 16 Feb 2006 12:09:07 GMT
Server: Apache/1.3.29 (Unix) mod_ssl/2.8.16 OpenSSL/0.9.7c
Location:
Content-Type: text/html
HTTP/1.1 200 OK
Content-Type: text/html

<script>...</script>
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html

0
*/
header('Location: index.php?id='.$_GET['id']);

Należy:

  • przefiltrować przychodzące dane, które mają być wykorzystane przy funkcji header(), pod względem znaków CR (%0d = \r) oraz LF (%0a = \n); pomocne w tym miejscu okażą się funkcje str_replace(), strtr() czy preg_match().

Nie należy:

  • uważać, że wykorzystanie addslashes() coś w tym miejscu pomoże – nie pomoże.

7) Directory traversal:

Błąd powiązany z Code Injection, pozwalający na przeglądanie systemu plików. Jest to dość poważny błąd, a w połączeniu z nieprawidłową konfiguracją serwera, może spowodować, że każdy będzie mógł uzyskać dostęp do kluczowych plików konfiguracyjnych serwera, z /etc/passwd włącznie.

// GET /index.php HTTP/1.0
// Cookie: skin=../../../../../../../../etc/passwd
$skin = isset($_COOKIE['skin']) ? $_COOKIE['skin'] : `default.skin';
readfile(`gfx/skins/'.$skin);

Należy:

  • filtrować dane przychodzące pod względem wystąpienia takich znaków, jak “..”, “../”, “..\”
  • wykorzystywać funkcję basename() lub instrukcję switch, do jednoznacznego określenia, jakie pliki mogą być wykorzystane w danej części kodu.

8) E-mail injection:

Często spotykany błąd, zazwyczaj przy wszelkiego rodzaju formularzach kontaktowych. Pozwala na dołączenie dowolnych nagłówków do przesyłanego e-maila, co w rezultacie powoduje, że możliwe staje się przesyłanie spamu czy nawet dołączenie dowolnej treści do przesyłanego tekstu wiadomości.

/* mail(
'webmaster @ example.com',
'Formularz na example.com',
$_POST['message'],
'From: '.$_POST['from']
);

$_POST['from'] = "user @ server.com%0ACc:user2 @ server2.com%0AB \
cc:user3 @ server3.com, user4 @ server4.com"

Wynikowe nagłówki wiadomość:
To: webmaster @ example.com
Subject: Formularz na example.com
From: user @ server.com
Cc:user2 @ server2.com
Bcc:user3 @ server3.com, user4 @ server4.com
*/

Należy:

  • filtrować dane, które mają być przesłane w nagłówku wiadomości pod względem wystąpienia znaków “\n” oraz “\r\n”; przydatne w tym miejscu są funkcje str_replace(), str_tr() czy strpos().

9) Session fixation:

Bardzo niebezpieczny błąd polegający na przejęciu kontroli nad kontem zalogowanego użytkownika, po poznaniu jego identyfikatora sesji. Jest to dość rozległy problem, a skutki jego wystąpienia mogą być naprawdę poważne. Jedną z metod przeprowadzenia tego typu ataku jest przesłanie użytkownikowi adresu URL z przekazanym już numerem identyfikacyjnym sesji, np. http://example.com/?SID=1234567890. W momencie, kiedy atakowana osoba zaloguje się, uzyskujemy dostęp nad jej kontem po prostu odświeżając stronę. Druga metoda, wykorzystywana gdy dane sesji są chronione zmienną kontrolną, polega na zalogowaniu się do serwisu i dopiero wtedy przesłaniu adresu URL do osoby atakującej, reszta działań pozostaje taka sama.

Należy:

  • przyjmować tylko identyfikatory sesji tworzone przez serwer, tworząc zmienną kontrolną:
    if (!isset($_SESSION['foo']))
    session_destroy();
    session_regenerate_id();
    $_SESSION['foo'] = 1;
    
  • sprawdzać, czy sesja należy do użytkownika, który żąda jej danych; jedną z możliwości jest zapisanie w sesji numeru IP czy łańcucha znaków określającego przeglądarkę użytkownika.
  • generować nowy identyfikator sesji przy każdym wywołaniu za pomocą funkcji session_regenerate_id(); dane sesji pozostają nadal dostępne, a sam atak staje się praktycznie niemożliwy do wykonania.

10) Session poisoning:

Błąd pozwalający użytkownikowi na zmianę dowolnej zmiennej sesji. Występuje, gdy tworzymy zmienną sesji na podstawie danych przekazanych w żądaniu.

// $_GET['var'] = "isadmin"
$_SESSION[$_GET['var']] = $_GET['value'];

Należy:

  • wykorzystywać instrukcję switch lub if w celu określenia jakie nazwy zmiennych mogą zostać przekazane w żądaniu.

11) Session injection:

Błąd występujący przy źle skonfigurowanych serwerach, pozwalający uzyskać dostęp do danych wszystkich zapisanych na serwerze sesji (również stworzonych przez serwisy innych kont) z poziomu kodu PHP lub innego. W skrajnych przypadkach pozwala na tworzenie i modyfikowanie danych sesji.

glob(ini_get('session.save_path').'/*');

Należy:

  • sprawdzić, czy błąd tego typu występuje na naszym serwerze, w razie czego powiadomić administratora.
  • wykorzystać własny mechanizm zapisu danych sesji, np. w bazie danych, zapisanych w postaci zaszyfrowanej.

Bonus (Przepis na SQL Injection) :Patrząc na niektóre kody źródłowe, również po zrobionych audytach bezpieczeństwa, można dojść do wniosku że SQL Injection jest dla niektórych programistów jakąś barierą nie do przejścia, bo praktycznie zawsze, jeśli kody źródłowe jakiegoś serwisu zawierają błędy, to znajdzie się tam dziura tego typu. Postanowiłem więc napisać jedną funkcję, która eliminuje problem SQL Injection:

<?php

    $arrArguments = array();
    $intArgumentIndex = 0;

    function parseArgument($arrMatches) {
        global $arrArguments, $intArgumentIndex;

        $strMatch = $arrMatches[0];
        $strArgument = @$arrArguments[$intArgumentIndex++];
        switch ($strMatch) {
            case '%d': return (int)$strArgument;
            case '%s': return '"'.mysql_real_escape_string($strArgument).'"';
            case '%b': return (int)((bool)$strArgument);
        }
    }

    function SQL($strSql) {
        global $arrArguments, $intArgumentIndex;

        $arrArgs = func_get_args();
        array_shift($arrArgs);
        $arrArguments = $arrArgs;
        $intArgumentIndex = 0;
        return preg_replace_callback('/(%[dsb])/', 'parseArgument', $strSql);
    }

?>

Funkcja SQL przyjmuje jeden wymagany parametr – zapytanie SQL w którym wykorzystujemy łańcuchy typu %d, %s czy %b do określenia typu wartości, jaka ma się w tym miejscu znaleźć (kolejno – liczba, łańcuch znaków i wartość typu boolean). Przyjmujemy magic_quotes_gpc=off. W praktyce wykorzystanie funkcji jest niezwykle proste:

// Przyjmujemy, dla przykładu:
$_POST['uid'] = 1;
$_POST['name'] = 'Łukasz "anAKiN" Lach';
$_POST['username'] = 'anakin';
$_POST['password'] = 'an4kin';
$_POST['newsletter'] = 1;

$sql = SQL('INSERT INTO users (id, uid, name, username, password, newsletter) '.
   'VALUES (NULL, %d, %s, %s, %s, %b)', $_POST['uid'], $_POST['name'],
   $_POST['username'], md5($_POST['password']), $_POST['newsletter']);

Wynikowa zawartość zmiennej $sql wygląda następująco:

INSERT INTO users (id, uid, name, username, password, newsletter)
VALUES (NULL, 1, "Łukasz \"anAKiN\" Lach", "anakin",
"97296eca657a093aa379778c237e292d", 1)

Chyba dość proste, a w każdym razie – skuteczne, zawsze.

Więcej informacji: PHP, OWASP How to secure PHP application using security functions

Kategorie K a t e g o r i e : Bezpieczeństwo

Tagi T a g i : , , , , , ,

Komentowanie tego wpisu jest zablokowane.