Rozpoznawanie typów plików w PHP

Gdy umożliwiamy na naszej stronie internetowej lub w systemie wgrywanie plików, powinniśmy sprawdzać ich typ. Dzięki temu dopuścimy tylko wybrane przez nas rodzaje, np. pliki graficzne. Możemy sobie jednak nie zdawać sprawy, że podstawowe, najczęściej stosowane możliwości sprawdzania typu są bardzo zawodne i umożliwiają wgranie na serwer niepożądanych danych.

Sprawdzanie rozszerzenia pliku

Najprostszym możliwym testem dla pliku jest sprawdzenie jego rozszerzenia. Oczywiście w ten sposób jest on wykonywany niezwykle rzadko. Wystarczy, że rozdzielimy nazwę względem kropki i sprawdzimy ostatnią z części. Jest to oczywiście sposób bardzo zawodny, gdyż ktoś może zmienić rozszerzenie przed wgraniem na serwer. Na przykład plik PHP może zostać ukryty pod nazwą image.jpg. Wykorzystując następnie luki w naszym systemie, istnieje możliwość wykonania takiego kodu.

Sprawdzanie typu MIME w tablicy $_FILES

Znacznie częściej stosowanym sposobem na rozpoznawanie rodzaju pliku jest sprawdzenie typu MIME. Możemy to osiągnąć, analizując dane z formularza, konkretnie wartość $_FILES['type'].

Poniższy, bardzo uproszczony kod, wyświetla formularz HTML z jednym polem umożliwiającym wczytanie pliku. Po jego wysłaniu wyświetlone zostaną szczegółowe dane o pliku z tablicy $_FILES.

<html>
    <body>
        <form action="mime_test.php" method="post" enctype="multipart/form-data">
        	<label for="file">Filename:</label>
        	<input type="file" name="file" id="file" />
        	<input type="submit" name="submit" value="Submit" />
        </form>
    </body>
</html> 

<?php 
if($_POST){
    print_r($_FILES);    
}
?>

Wczytanie przykładowego zdjęcia zwróci w polu type wartość image/jpeg. Co się jednak stanie, gdy przez formularz wczytamy plik PHP ze zmienioną nazwą na jpg? Wynik może być dla niektórych zaskakujący – powyższy kod wyświetli informację, że wgrany został plik graficzny.

Dlaczego tak się dzieje? Otóż dane na temat pliku przekazywane są przez przeglądarkę, a większość z nich (wszystkie?) określa typ MIME po rozszerzeniu pliku. Tak więc wracamy do punktu wyjścia.

Wykrywanie pliku graficznego

Zawodność powyższych metod możemy naprawić jednym prostym rozwiązaniem. Wystarczy, że skorzystamy z funkcji getimagesize() z biblioteki GD, która zwraca wymiary pliku graficznego. Przykładowe wywołanie wraz z efektem może wyglądać nastepująco:

print_r(getimagesize('image.jpg'));

Array
(
    [0] => 979
    [1] => 734
    [2] => 2
    [3] => width="979" height="734"
    [bits] => 8
    [channels] => 3
    [mime] => image/jpeg
)

Otrzymujemy więc dane na temat wymiarów grafiki, palety kolorów, liczby kanałów oraz typu MIME. Jeśli spróbujemy naszego oszustwa ze zmianą rozszerzenia i użyjemy powyższej funkcji do sprawdzenia spreparowanego pliku PHP, funkcja zwróci wartość false.

var_dump(getimagesize('phpcode.jpg'));
bool(false)

Dzięki temu możemy sprawdzić, czy wgrywany plik jest faktycznie obrazkiem. Co jednak zrobić, gdy chcemy testować inne typy?

Sprawdzanie typu pliku po „magicznych bajtach”

Każdy plik zaczyna się od charakterystycznych bajtów, które identyfikują jego typ. Dla kilku popularnych typów, wyglądają one następująco (w zapisie szesnastkowym):

GIF87a:  47 49 46 38 37 61 
GIF89a:  47 49 46 38 39 61 
JFIF, JPE, JPEG, JPG: FF D8 FF E0 xx xx 4A 46 49 46 00 
AVI: 52 49 46 46 xx xx xx xx 41 56 49 20 4C 49 53 54 

Wystarczy więc je wczytać i sprawdzić, aby określić rodzaj. Na poniższym przykładzie przedstawiono sprawdzenie, czy dany plik to gif.

<?php
$file = fopen('image.gif', 'r');
$bytes = bin2hex(fread($file, 6));

if($bytes == '474946383761' || $bytes == '474946383961'){
    print 'To plik gif';
}
else{
    print 'To NIE JEST gif';
}

fclose($file);
?>

Działanie kodu jest niezwykle proste. Wczytujemy pierwsze 6 bajtów z pliku, po czym konwertujemy je na wartości hexadecymalne. Następnie porównujemy z odpowiednim schematem, który dla GIF może być 47 49 46 38 37 61 lub 47 49 46 38 39 61.

Długa lista „magicznych bajtów” dla najróżniejszych typów plików jest dostępna tutaj.

Możemy na jej podstawie stworzyć własną funkcję sprawdzającą. Oczywiście to także nie zabezpieczy nas w stu procentach. Możliwe jest bowiem stworzenie pliku z oszukanym nagłówkiem. Ten sposób, mimo że dość karkołomny, jest jednak znacznie lepszy od poprzednich.

Sprawdzanie typu przez Fileinfo

Niedogodności poprzednio opisanego sposobu można zlikwidować, stosując wbudowane w PHP funkcje Fileinfo. Są one obecne od wersji 5.3.0 języka, dla wcześniejszych możemy skorzystać z PECL. Oczywiście podczas kompilacji PHP należy się upewnić, że dołączamy powyższą bibliotekę.

Poniższe wywołanie pokazuje efekt sprawdzenia typu pliku jpg oraz PHP ze zmienionym rozszerzeniem.

<?php 
$file1 = 'image.jpg';
$file2 = 'phpcode.jpg';

$file_info = new finfo(FILEINFO_MIME);

print $file_info->file($file1, FILEINFO_MIME_TYPE); 
print $file_info->file($file2, FILEINFO_MIME_TYPE);
?>

Wynik wywołania skryptu:

image/jpeg
text/x-php

Wywołanie metody file posiada kilka innych wariantów, w zależności od drugiego parametru. Możemy między innymi sprawdzić kodowanie pliku oraz klika innych rzeczy.

Artykuł został oparty o tekst z bloga http://cakephp-php.blogspot.com.

Dodaj komentarz