Aplikacje internetowe

Bełdziowe spojrzenie na aplikacje internetowe

Bezpieczny upload plików

Dzisiejsza notka będzie składała się z dwóch części. W pierwszej z nich zostaną zaprezentowane słabości wykorzystywanych często sposobów weryfikacji uploadowanych plików. Druga część będzie koncentrowała się na technikach, których wdrożenie przyczyni się do znacznego wzrostu bezpieczeństwa mechanizmu wgrywania plików.

Słabości mechanizmu uploadu

Większość mechanizmów weryfikacji wgrywanych plików opiera się na jednej z trzech metod – sprawdzaniu rozszerzenia pliku, weryfikacji MIME-Type przesłanego pliku oraz korzystaniu z funkcji getimagesize.

Sprawdzanie rozszerzenia pliku

Najprostszym sposobem weryfikacji typu przesłanego pliku jest sprawdzenie jego rozszerzenia. Możemy tego dokonać korzystając z funkcji substr oraz strrpos w celu pobrania znaków znajdujących się po ostatniej kropce.

$fileName = $_FILES[ 'file' ][ 'name'];
$ext = substr( $fileName, strrpos( $fileName, '.' ) +1 );

Następnie musimy określić sposób weryfikacji rozszerzeń. Możemy ustalić jakie rozszerzenia mogą zostać przesłane lub jakie nie mogą ( white / black list ).

W przypadku określenia jakie typy plików nie mogą zostać wgrane możemy w łatwy sposób zablokować wszelkiego typu skrypty, które mogłyby dokonać szkód na naszym serwerze. Przykładowo jeśli mamy zainstalowany tylko interpreter PHP, do którego kierowane są wszystkie pliki z rozszerzeniem *.php, *.phtml, *.inc możemy określić, że każdy plik, który ma inne rozszerzenie może zostać wgrany. W tak prosty sposób uniemożliwiliśmy wgranie skryptów PHP na serwer. Ale czy na pewno?

Aby obejść powyższe zabezpieczenie wystarczy wgrać plik .htaccess o następującej zawartości:

AddType application/x-httpd-php .txt

Wgranie powyższego pliku spowoduje przesyłanie wszystkich plików w rozszerzeniem *.txt do interpretera PHP, a co za tym idzie traktowaniu ich jak zwykłe skrypty. Dzięki czemu wgrywając dowolny plik tekstowy uzyskujemy możliwość wykonywania skryptów PHP.

W przypadku zastosowania białych list sprawa jest dużo prostsza. Jeśli rozszerzenie pliku nie spełnia zdefiniowanych warunków plik nie zostaje zapisany. Niby wszystko działa tak jak powinno, jednak bezpieczeństwo skryptu jest w pełni zależne od konfiguracji serwera HTTP. Wystarczy, że w jego konfiguracji zostanie zdefiniowanie traktowanie plików graficznych jako skryptów (co wcale nie zdarza się tak rzadko) i nasze zabezpieczenie przestaje spełniać swoją funkcję.

Sprawdzanie typu MIME

Kolejnym często stosowanym wyznacznikiem typu przesyłanego pliku jest jego typ MIME. MIME-Type jest informacją dołączaną przez przeglądarkę określającą zawartość pliku. Przykładowo wysyłając obrazek z rozszerzeniem *.gif przeglądarka określa jego typ jako image/gif. Chcąc umożliwić wgranie dowolnego pliku będącego obrazkiem możemy ustalić, że akceptujemy wszystkie pliki, których typ rozpoczyna się od image/.

$mime = $_FILES[ 'file' ][ 'type'];

if( strpos( $mime, 'image/' ) === false )
   die( 'Wybrany plik nie jest obrazkiem.' );

Powyższe rozwiązanie ma tylko jedną wadę. Typ MIME przesyłany jest przez przeglądarkę, czyli jest on zmienną zewnętrzną. Jako, że pierwsza zasada bezpieczeństwa aplikacji internetowych mówi o braku zaufania do danych przychodzących z zewnątrz nie mamy pewności, że informacja ta jest wiarygodna.

Wykorzystując prosty skrypt Perla możemy w dowolny sposób manipulować typem przesyłanego pliku, dzięki czemu skrypt PHP może bez problemy być rozpoznany jako obrazek.

use LWP;
use HTTP::Request::Common;

$ua = LWP::UserAgent->new;

$result = $ua->request( POST 'http://localhost/file-upload/index.php',
                         Content_Type => 'form-data',
                         Content      => [
                                            file => [ 'phpinfo.php',
                                                       'phpinfo.php',
                                                       'Content-Type' => 'image/gif' ]
                                         ],
                       );
print $result->as_string( );

Weryfikacja funkcją getimagesize

Chcąc pobrać od użytkownika obrazek najprostszym sposobem jego weryfikacji jest wykonanie funkcji getimagesize przekazując do niej nazwę przesłanego pliku. W przypadku, gdy jej wynikiem będzie wartość inna niż false mamy pewność, że plik jest poprawnym obrazkiem.

Pojawia się tylko pytanie czy plik może być jednocześnie poprawnym obrazkiem i skryptem PHP?

Większość plików graficznych w swojej strukturze ma zarezerwowane miejsce na komentarz, w którym bez problemu można umieścić krótki skrypt. W wyniku czego uzyskujemy poprawny plik graficzny, który zawiera w sobie poprawny skrypt PHP. Oczywiście, aby skrypt się wykonał obrazek musi przejść przez interpreter PHP.

Zwiększenie bezpieczeństwa uploadu

Zabezpieczenie katalogu przechowującego pliki

Proces zabezpieczenia katalogu przed bezpośrednim dostępem do jego plików został opisany w notce Bezpieczeństwo dostępu do plików.

Uprawnienia plików

Innym sposobem zablokowania bezpośredniego dostępu do plików jest określenie ich praw dostępu. Podczas uploadu pliku ustawiane są mu prawa umożliwiające jego odczyt przez każdego użytkownika. Wystarczy zmienić je z 644 na 600, a dostęp ten zostanie ograniczony jedynie dla skryptów znajdujących się na serwerze.

$fileName = $_FILES[ 'file' ][ 'name'];
$ext = substr( $fileName, strrpos( $fileName, '.' ) );
$newFileName = 'upload/' . md5( time( ) . $_FILES[ 'file' ][ 'name' ] ) . $ext;

if( is_uploaded_file( $_FILES[ 'file' ][ 'tmp_name' ] ) )
   if( move_uploaded_file( $_FILES[ 'file' ][ 'tmp_name' ], $newFileName ) )
      chmod( $newFileName, 0600 );

Określenie uruchomienia dozwolonego typu plików

Nawet w przypadku, gdy katalog z uploadowanymi plikami dostępny jest publicznie oraz nie posiadamy żadnej weryfikacji typu przesłanego pliku możemy w prosty sposób uchronić się przed uruchomieniem pliku z rozszerzeniem innym niż dozwolone przez nas. Wystarczy w powyższym katalogu umieścić plik .htaccess o następującej treści:

   order deny,allow
   deny from all

Od tej pory wywołanie pliku z rozszerzeniem innym niż określone powyżej będzie skutkowało błędem odmowy dostępu.

Wyłączenie możliwości uruchomienia skryptu

Powyższy efekt możemy również osiągnąć blokując możliwość wykonywania skryptów.

Options -ExecCGI
AddHandler cgi-script .php .asp .py

Wyświetlanie skryptu jako plik tekstowy

Co prawda skrypty są zwykłymi plikami tekstowymi jednakże mają pewną właściwość, która sprawia, że nie są do końca takie zwykłe – zamiast prostego wyświetlenia przechodzą przez interpreter. Możemy jednak wymusić wyświetlanie ich w oryginalnej formie. Wystarczy w pliku .htaccess umieścić następujące dyrektywy:

   ForceType text/plain

Zmiana nazwy pliku na losową

W przypadku, gdy katalog z wgranymi plikami jest dostępny publicznie lub pliki wyświetlane są na podstawie oryginalnej nazwy istnieje możliwość dostępu do dowolnego pliku przez dowolnego użytkownika. W chwili, gdy walidacja typu pliku zostanie ominięta nic nie stoi na przeszkodzie, aby napastnik uruchomił wgrany plik. Wystarczy jednak zamienić nazwę pliku na losowy ciąg znaków, aby mimo obejścia zabezpieczeń nie było możliwe uruchomienie pliku – można wyszukać nazwy pliku metodą brute force jednak jest to bardzo nieoptymalne zadanie.

$fileName = $_FILES[ 'file' ][ 'name'];
$ext = substr( $fileName, strrpos( $fileName, '.' ) );

if( is_uploaded_file( $_FILES[ 'file' ][ 'tmp_name' ] ) )
   move_uploaded_file( $_FILES[ 'file' ][ 'tmp_name' ], 'upload/' . md5( time( ) . $fileName ) . $ext );

Tagi: , ,
Kategoria: Bezpieczeństwo, Apache, Bezpieczeństwo, Upload


14 komentarzy

  1. a co powiesz aby przy uploadzie grafiki zamiast bawić się z move_uploaded_file dać GD2 ?
    robić kopię jak przy miniaturkach i dopiero kopię zapisywać.
    jeśli nie będzie obrazem nie zapisze .

    pozdrawiam,

    Spawnm

  2. też można :) sam z tego korzystam jako jednego z elementów uploadu :)

  3. Szymek napisał(a):

    Tutaj byłem i komentarzyk zostawiłem.

  4. bimas napisał(a):

    Funkcja mime_content_type(), której zwracane dane zależne są od konfiguracji parsera PHP sprawdza typ MIME pliku ;)

  5. rzut oszczepem przez kibel na czas napisał(a):

    Cześć Bełdzio :-)

    Mam problem. Chciałbym umożliwić użytkownikowi mojej aplikacji internetowej wpisywanie skryptu PHP, który później będzie wykonany. Ale chciałbym, żeby użytkownik miał dostęp tylko do jednego folderu z poziomu tego skryptu i nie miał dostępu do bazy danych. Pierwsze nasuwające się rozwiązanie tego problemu jest takie, że po prostu sprawdzamy ten skrypt, czy nie ma tam jakichś funkcji wczytujących lub zapisujących coś do jakiegoś pliku, w którym nie wolno mu grzebać i czy w tym skrypcie nie ma nic o bazie danych. Czy jest jakieś lepsze rozwiązanie? Da się jakoś określić, jakie pliki i foldery dany plik PHP może otwierać, a jakie nie?

  6. hmm jeśli chodzi o połączenie z bazą danych możesz pokombinować z konfigiem PHP i dyrektywą disable_functions, jeśli chodzi o dostęp do katalogów to podobnie trzebaby poszukać odpowiedniej dyrektywy w konfigu php / apache i porobić coś na kształt wirtualnych kont :)

  7. rzut oszczepem przez kibel na czas napisał(a):

    To chyba najprostszym rozwiązaniem będzie po prostu moderacja tych skryptów PHP pisanych przez użytkownika :-)

  8. Tej podpowiedzi bezpiecznego uploadu wlaśnie szukałem.

  9. rzut oszczepem przez kibel na czas:

    Jedno z rozwiązań jest dość zakręcone, mianowicie zastosowanie generatora skryptów php który tylko sugerowałby się skryptem wrzuconym przez użytkownika.
    Użytkownik wrzuca swój skrypt, Twoj parserogenerator analizuje go i na podstawie jego treści tworzy już zmoderowany skrypt. Rzecz w tym żeby w żaden sposób nie umożliwić przepuszczenia jakichkolwiek hacków. Twój skrypt używałby tylko sprawdzonych, dozwolonych funkcji, w sposób na jaki Ty zezwolisz. Każda dziwna, niezrozumiała, szkodliwa czy głupia instrukcja ze skryptu użytkownika zostanie pominięta. (niezłe brednie mogą wyjść, trzeba go odesłać userowi do sprawdzenia)
    Najpoważniejsza wada to kupa roboty, równie dobrze można opracować własny język skryptowy, chociaż jako końcowy użytkownik wolałbym chyba okrojone PHP.

  10. Wszystko ok. Jednak skrypt perlowy nie pozwolił mi na upload innego typu pliku. Albo ja coś źle robię albo zabezpieczenia są w miarę dobre :) Mimo wszystko notka przydatna.

  11. Tadek napisał(a):

    Bardzo ciekawy artykuł – już nie po raz pierwszy czytam beldzio.com

    Jeśli chodzi o sprawdzanie formatu pliku to chyba lepiej zrobić to przez explode

    $filename = moj.avatar.jpg
    $make = explode(‚.’, $filename);
    $format = strtolower(end($make));
    if(($format==’jpg’)||($format==’jpeg’)){ ….

    w razie gdyby w nazwie pojawiło się więcej kropek :) przeskakujemy na ostatnią wartość w tablicy explode end’em .

    A teraz pytanko.
    Jeśli chodzi o komentarz pliku jpg/jpeg.
    Jak przez php go wyczyścić i inne wartości które mogą być skryptem ?
    Jakieś słowo kluczowe dla google :) ?

    Pozdrawiam

  12. Dzięki :) Sprawdzanie rozszerzenia pliku zostało opisane w pierwszym akapicie wpisu. Jeśli zastosujesz białą listę rozszerzeń jesteś z grubsza bezpieczny. Jeśli chodzi o usuwanie komentarzy to sugeruje zastosowanie techniki, którą w pierwszym komentarzu opisał Spawnm.

  13. Tadek napisał(a):

    Witam to jeszcze raz ja :)
    Z takim zapytaniem. W dobie lustrzanek i innych dobrodziejstw ludzie coraz częściej mają fotki po 5000x4000px. Czego użyć aby można był je zmniejszyć bo moduł GD odpadł na starcie chyba że trzeba ustawić większą pamięć serwera. Zastosować imagemagick czy może jakąś bibliotekę zewnętrzną ?

    Pozdrawiam

  14. IM powinien sobie poradzić.

Dodaj komentarz