Skip to main content

Bezpieczne wysyłanie plików na serwer

Umożliwienie użytkownikowi wysyłania plików na stronie(serwer www) może mieć katastrofalne konsekwencje, jeśli zamiast spodziewanego obrazka, użytkownik wyśle plik wykonywalny (np. PHP), który następnie uruchomi.

W tym artykule opiszę jak powinna wyglądać procedura obsługi formularza, który umożliwia wysyłanie zdjęć.

Jeśli chcesz przechowywać na serwerze oryginalne pliki obrazków bez ich przetwarzania (tworzenia miniaturek) narażasz się na dodatkowe niebezpieczeństwo, gdyż przechowujesz plik w niezmienionej postaci.

Nigdy nie ufaj przesyłanym zmiennym z formularza, nazwie pliku, a już tym bardziej jego rozszerzeniu.
Pola formularza mogą zostać zmodyfikowane lub usunięto po stronie klienta za pomocą np. Firefox Developer Tools.

Założenia

  • Obsługa plików JPG, GIF, PNG.
  • Maksymalny rozmiar pliku 4MB.

Przykładowy formularz

    <?php
        $customMaxFileSize = 4194304; // 4MB

        if (($upload_max_filesize = ini_get('upload_max_filesize')) && preg_match('/^(\d+)([KM])$/', $upload_max_filesize, $matches)) {
            switch($matches[2]) {
                case 'K':
                    $upload_max_filesize = $matches[1] << 10; // $matches[1] * 1024;
                    break;
                case 'M':
                    $upload_max_filesize = $matches[1] << 20; // $matches[1] * 1024 * 1024;
                    break;
            }
        } else {
            $upload_max_filesize = $customMaxFileSize;
        }

        if ($customMaxFileSize > $upload_max_filesize) {
            $customMaxFileSize = $upload_max_filesize;
        }
     ?>
<form role="form" method="post" action="" enctype="multipart/form-data">
    <input name="MAX_FILE_SIZE" type="hidden" value="<?php echo $customMaxFileSize; ?>">

    <label for="input-logo">Zdjęcie</label>
    <input id="input-logo" name="Logo" type="file">
</form>

Obsługa formularza

    // Katalog docelowy
    $combine = array(
        __DIR__,
        'uploads'
    );
    $uploadDir = implode(DIRECTORY_SEPARATOR, $combine);

    if (! file_exists($uploadDir)) {
        @mkdir($uploadDir);
    }

    // Czy wysłano plik?
    if (! empty($_FILES['Logo']) && $_FILES['Logo']['error'] != UPLOAD_ERR_NO_FILE) {

        // Czy katalog docelowy ma prawa do zapisu?
        // Czy plik został wysłany metodą POST?
        // Czy wysłanie pliku powiodło się?
        // Czy plik jest obrazkiem o odpowiednim typie?
        if (is_writable($uploadDir) && is_uploaded_file($_FILES['Logo']['tmp_name']) && $_FILES['Logo']['error'] == UPLOAD_ERR_OK && ($size = getimagesize($_FILES['Logo']['tmp_name'])) && in_array($size[2], array(
            IMAGETYPE_JPEG,
            IMAGETYPE_GIF,
            IMAGETYPE_PNG
        ))) {

            // Dodaje poprawne rozszerzenie, nawet jeśli zostało podane nieprawidłowe lub wcale.
            switch ($size[2]) {
                case IMAGETYPE_JPEG:
                    $ext = '.jpg';
                    break;
                case IMAGETYPE_GIF:
                    $ext = '.gif';
                    break;
                case IMAGETYPE_PNG:
                    $ext = '.png';
                    break;
                default:
                    $ext = '';
            }

            // Teraz można bezpiecznie skopiować oryginalny obrazek.
            // Możesz użyć funkcji move_uploaded_file(), która dodatkowo sprawdza, czy plik został wysłany metodą POST.
            // Jednak w tym konkretnym przypadku już to sprawdziliśmy za pomocą funkcji is_uploaded_file().
            copy($_FILES['Logo']['tmp_name'], $uploadDir. DIRECTORY_SEPARATOR . 'original_logo' . $ext);

            // Tutaj możesz wykonać dodatkowe operacje na pliku, np. stworzyć miniaturkę z pliku źródłowego $_FILES['Logo']['tmp_name'] lub powyższej kopii.

        } else {
            // Nie udało się wysłać pliku.
        }
    } else {
        // Nie wysłano żadnego pliku.
    }

Aby wyświetlić komunikaty z błędami, wypadałoby rozbić jeden długi warunek na kilka mniejszych i zwrócić odpowiedni komunikat.
Jedną z ważniejszych funkcji jest tutaj getimagesize(), która sprawdza czy plik jest w ogóle obrazkiem.
Zwraca ona tablicę parametrów, gdzie:

  • index 0 – szerokość
  • index 1 – wysokość
  • index 2 – typ
  • index 3 – wysokość i szerokość zgodna dla tagu IMG (height=”yyy” width=”xxx”)
  • index 'mime’ – typ mime dla Content-Type

Ważne jest, aby sprawdzić wszystkie możliwe przypadki, które mogą spowodować błąd, np. usunięcie pola formularza z drzewa DOM, zastąpienie zmiennej MAX_FILE_SIZE przez użytkownika.

Jak już zapewne zauważyłeś, ten ostatni warunek nie jest nigdzie sprawdzany (nie licząc warunku po stronie klienta), dlatego należy dodać dodatkowy warunek sprawdzający wielkość pliku:

    if ($_FILES['Logo']['size'] <= $customMaxFileSize) {
       // OK
    }

Jeśli chodzi o indeks „size” to jest on bezpieczny i zwraca wielkość pliku po wgraniu na serwer, w odróżnieniu od „mime”, który odzwierciedla typ pliku i może zostać zmanipulowany przez użytkownika wysyłającego plik, dlatego nie należy na nim polegać!
W skrypcie użyliśmy już do tego funkcji getimagesize().

W przypadku innych typów plików, możemy użyć poniższego kodu w celu pobrania typu MIME:

    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $_FILES['Logo']['tmp_name']);
    finfo_close($finfo);

W przypadku pliku JPG wynik zmiennej $mimeType może wyglądać następująco:

  • .jpeg image/jpeg
  • .jpeg image/pjpeg
  • .jpg image/jpeg
  • .jpg image/pjpeg

W zależności od przeglądarki (i/lub obrazka), różne wyniki mogą być zwracane dla tych samych plików (lub rozszerzeń).
Należy to mieć na względzie budując odpowiedni warunek.

Zapraszam do dyskusji w komentarzach na temat bezpieczeństwa skryptu.

markac

Full-stack Web Developer

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.