Czas na drugą część (tak, to oznacza że była część pierwsza ;>) posta o getimagesize. Tym razem skupie się bardziej na tym jak getimagesize robi to do czego została stworzona - czyli jak odczytuje wielkość z poszczególnych formatów graficznych. Będzie również o tym dlaczego getimagesize nie powinno się używać do sprawdzania czy uploadowany plik faktycznie jest obrazkiem.

Najpierw drobne uzupełnienie dot. notice'ów generowanych przez getimagesize: w wersjach PHP poniżej 5.2.6 błędy odczytu były sygnalizowane warningami, dopiero od wersji 5.2.6 został obniżony poziom istotności komunikatów o błędach odczytu.

OK, a teraz krótka lista formatów obsługiwanych przez getimagesize (w kolejności losowej):
- GIF
- JPEG
- PNG
- SWF
- SWC (od 4.3.0, wymaga statycznie zlinkowanego zlib)
- PSD (od 4.0.6)
- BMP (od 4.0.6)
- TIFF (od 4.2.0)
- JPC (od 4.3.2)
- JP2 (od 4.3.2)
- JPX (od 4.3.2)
- JB2 (od 4.3.2)
- IFF (od 4.3.0)
- WBMP (od 4.3.2)
- XBM (od 4.3.2)
- ICO (od 5.3.0)

W tym oraz kilku kolejnych postach opiszę implementacje poszczególnych funkcji wyciągających wielkość obrazka z danego formatu. Dzisiaj zacznę od formatu GIF, by w kolejnych postach omówić JPEG, ICO. BMP i inne.
Głównym celem na którym się skupie będzie uzyskanie ciągu bajtów który będzie poprawnie przechodził przez getimagesize ale nie będzie poprawnym obrazkiem, oraz na stworzeniu obrazka który ma inne wymiary (w sensie: przeglądarki traktują go jako np. większy) niż getimagesize zwraca.

1. GIF


Obsługą formatu GIF zajmuje się funkcja php_handle_gif, jednak aby do niej dojść, trzeba najpierw poprawnie przejść przez funkcje php_getimagetype która musi zwrócić IMAGE_FILETYPE_GIF. Aby tak się stało, poniższy warunek musi zostać spełniony:

if (!memcmp(filetype, php_sig_gif, 3)) {
 return IMAGE_FILETYPE_GIF;


Gdzie php_sig_gif to po prostu ciąg "GIF". Tak że php_getimagetype mamy załatwione, czas na php_handle_gif:

static struct gfxinfo *php_handle_gif (php_stream * stream TSRMLS_DC)
{
 struct gfxinfo *result = NULL;
 unsigned char dim[5];

 if (php_stream_seek(stream, 3, SEEK_CUR)) (1)
   return NULL;

 if (php_stream_read(stream, dim, sizeof(dim)) != sizeof(dim)) (2)
   return NULL;

 result = (struct gfxinfo *) ecalloc(1, sizeof(struct gfxinfo));
 result->width    = (unsigned int)dim[0] | (((unsigned int)dim[1])<<8);
 result->height   = (unsigned int)dim[2] | (((unsigned int)dim[3])<<8);
 result->bits     = dim[4]&0x80 ? ((((unsigned int)dim[4])&0x07) + 1) : 0;
 result->channels = 3; /* allways */

 return result;
}


Jak widać funkcja ta jest wyjątkowo krótka (w porównaniu do pełnej obsługi formatu GIF), i na dobrą sprawę opiera się na odczycie najpierw 3ch bajtów (if (1), to resztka magic'a GIFa, który mówi o wersji formatu; wg. standardu powinno tu być 87a lub 89a, ale jak widać w przypadku tej funkcji nie jest to wymagane), a następnie kolejnych 5ciu (if (2)). Po tych dwóch operacjach pola width/height/bits/channels struktury result są ustawiane wg. wartości odczytanych za drugim razem (konkretniej rzecz biorąc za drugim razem odczytywany jest fragment struktury Logical Screen Descriptor). I tyle.
Wniosek jest następujący: w przypadku formatu GIF (jak i innych formatów, jak okaże się później) getimagesize idzie po linii najmniejszego oporu, prosto do celu, ignorując wszystko inne.

W żadnym więc wypadku funkcja getimagesize nie może zostać wykorzystana do walidacji formatu GIF (nic dziwnego, w końcu ta funkcja nie została do walidacji stworzona, mimo iż niektórzy programiści używają ją właśnie w tym celu).

Przykładowy ciąg przechodzący poprawnie przez getimagesize, a na pewno nie będący poprawnym obrazkiem to:

$data = "GIFxxx" . pack("vvC", 1024, 768, 0xff);
$a = getimagesize("data://text/plain;base64," . base64_encode($data));
var_dump($a);


Wynik działania:
array(7) {
 [0]=>
 int(1024)
 [1]=>
 int(768)
 [2]=>
 int(1)
 [3]=>
 string(25) "width="1024" height="768""
 ["bits"]=>
 int(8)
 ["channels"]=>
 int(3)
 ["mime"]=>
 string(9) "image/gif"
}


OK, a co z obrazkiem który miałby inną wielkość niż zwraca getimagesize?
W tym momencie musimy poznać odpowiedź na pytanie: na ile możemy nagiąć standard GIF tak aby przeglądarka X wyświetliła obrazek poprawnie? Odpowiedź zależy oczywiście od wyboru przeglądarki X.
Oczywiście jest też drugie pytanie: jak nagiąć standard tak aby plik GIF był widziany powiedzmy jako 10x10 dla getimagesize, ale powiedzmy 256x192 dla przeglądarki?

W przypadku GIF może się okazać to dość łatwe. Osoby znające format GIF wiedzą że składa się on z jednego logicznego obrazka (którego wielkość jest właśnie odczytywana przez getimagesize), oraz z dowolnej ilości prostokątnych obrazków które są nakładane (wg. zaleceń zawartych w strukturach Image Descriptor) na obraz logiczny. Co dla nas ważne, wielkość każdego obrazka zapisywana jest oddzielnie!

Stwórzmy więc (można to zrobić np. przerabiając GIF hexedytorem) GIF'a który będzie miał wpisane 10x10 w Logical Screen Descriptor i 256x192 w Image Descriptor. Przy odrobinie szczęścia niektóre przeglądarki przeskalują Logical Screen do wielkości największego obrazka.

Przykładowy GIF (drogi czytelniku: jeżeli obrazek wyświetla się 256x192, to znaczy że Twoja przeglądarka powiększa Logical Screen do największego Image'a):

LSD mniejsze od ID, czyli gif i image size


(Obrazek oczywiście pochodzi z http://icanhascheezburger.com/ ;>)
Jak się okazuje (empirycznie) powyższy obrazek wyświetlany jest jako 256x192 przez Firefox'a, Operę oraz Google Chrome. Natomiast Konqueror obcina obrazek do 10x10, a IE odmawia jego wyświetlenia (rezerwując jednak prostokąt wielkości 256x192 na potrzeby jego wyrenderowania).
Wynik getimagesize dla powyższego obrazka:

array(7) {
 [0]=>
 int(10)
 [1]=>
 int(10)
 [2]=>
 int(1)
 [3]=>
 string(22) "width="10" height="10""
 ["bits"]=>
 int(8)
 ["channels"]=>
 int(3)
 ["mime"]=>
 string(9) "image/gif"
}


I to by było na tyle...

P.S. Uprzedzając pytanie: taaak, sporo skryptów PHP korzysta z getimagesize żeby sprawdzić czy obrazek wrzucany przez user'a nie jest aby za duży. Guess they don't work too well, now do they?

Comments:

2009-08-25 08:23:21 = carstein
{
Warto jeszcze wspomnieć, że w GIFach można całkiem fajnie wrzucić kod PHP. Taki gif przejdzie walidację funkcją getimgsize(), rozszerzeniem itp, natomiast jeśli uda nam się odwołać do obrazka poprzez funkcję include (i podobne) to mamy RFI.

(To samo z XSS'ami)
}
2009-08-25 11:45:14 = Gynvael Coldwind
{
Yep, w zasadzie dokładnie w tym celu pokazałem jak krótki ciąg bajtów może zostać oznaczony jako 'OK' przez getimagesize(). A po tym krótkim ciągu już może być cokolwiek, choćby kod PHP jak carstein pisze ;>
}
2009-08-25 12:42:16 = Neonek
{
Ciekawy sposób na zdalne includowanie kodu. Jeśli wolno spytać, jak się bronić od takich specjalnie spreparowanych obrazków ? Ten zwierz na obrazku (latest Firefox: 256x192) wygląda zupełnie jak moja kotka :D
PS Kiedy następny ReverseCraft ?
}
2009-08-25 13:11:15 = Malcom
{
A ja dalej nie rozumiem, jak mozna dopuscic, do includowania w skrypcie czegokolwiek uploadowanego przez usera :>
}
2009-08-25 13:53:33 = Gynvael Coldwind
{
@Neonek
Dla jasności, samo uploadowanie kodu to jedno, a metoda na include kodu to zupełnie inna sprawa. I o ile powyższa metoda może ułatwić upload kodu na serwer, to atakujący i tak by musiał jakimś LFI dysponowac żeby coś napsuć ;>
Co do "jak się bronić": sprawa jest bardzo trudna szczerze mówiąc. Najłatwiej jest wczytać obrazek jakimś loaderem (GD?) i go ponownie zapisać na dysk (żeby wymusić re-encoding obrazka). Wtedy ofc wszystkie meta-informacje idą papa, a niepoprawne obrazki się w ogóle nie wczytają (pytanie czy nie otworzy to kolejnej drogi do kolejnych warningów / bugów).
Z drugiej strony, nawet poprawny obrazek może zawierać kod PHP (jako tekst ofc), i "konwersja" (aka ponowny encoding) może dużo nie pomóc (to akurat testowałem he he). Więc może trzeba by jeszcze szukać jakiegoś <? czy <?php w danych obrazka? A co jeśli taki ciąg pojawi się w prawidłowym normalnym obrazku?
Jak pisałem, trudna sprawa ;>

@Malcom
Dokładnie ;>
}
2009-08-25 15:02:18 = carstein
{
Pewnym rozwiązaniem jest transformacja obrazka (czasami cross-format) i uniemożliwienie uzytkownikowi bezpośrednie odwołanie się do obrazka. Widziałem takie rozwiązanie oparte o imagemagic i ładnie eliminowało wszelkie możliwości XSS'a (LFI nie działał, bo to był inny język niż PHP).
}
2009-08-26 11:48:51 = TeMPOraL
{
explorer.exe w Windows Vista nie pokazuje wymiarów testowego GIF'a. IfranView (4.23) traktuje go jako 256x192 i takie też dane podaje w informacjach o obrazku. Podobnie zachowuje się telefon SE K800i - wyświetla 256x192 i tyle też podaje w informacjach jako jego wymiary. Ciekaw jestem, czy sprytnie spreparowane obrazki mogłyby spowodować jakieś ciekawe efekty uboczne na telefonach komórkowych.
}
2009-08-27 06:26:49 = Gynvael Coldwind
{
@carstein
To jest bardzo ciekawy temat na research imo (już kiedyś o tym myślałem). Chodzi mi ofc o modelowanie danych wejściowych w takich sposób by po serii transformacji uzyskać dany ciąg znaków. Nie sądzę by to było trywialne, ale uważam że jednak możliwe ;> Może poświęcę temu jakiś post w swoim czasie ;>

@TeMPOraL
Thx za dane ;> Sądzę że jest tak jak mówisz, tj. odpowiednio spreparowane obrazki mogą wywołać bardzo ciekawe efekty również w telefonach komórkowych ;>

Z ciekawostek, gqview jest niezwykle niezdecydowany, tj przy odpaleniu "gqview obrazek.gif" wyświetla prostokąt wielkości 256x192, który w większości jest czarny, po czym rysuje tylko 10x10 pixeli obrazka na nim. Co ciekawe, w podglądzie plików w katalogu (panelik po prawej stronie) obrazek jest widoczny cały, a i jak się wybierze obrazek z tego panelu (a nie z linii poleceń) to pokazuje się całe 256x192. Zachowanie bardzo oryginalne muszę przyznać ;>
GIMP natomiast zachował się w sposób bardzo profesjonalny, i imo najlepszy z możliwych. Mianowicie utworzył obrazek 10x10 po czym wstawił warstwę wielkości 256x192. Tak że naraz widać 10x10, ale warstwę można przesuwać, a obrazek reskalować, tak że w końcu można zobaczyć całość ;> Punkt dla GIMP'a.
Dolphin (coś jak "explorer.exe" pod KDE) pokazuje 10x10, tak samo Gwenview.
OpenOffice.org natomiast pokazuje 256x192 (czyli tak jak Firefox/Opera).
Co program to pomysł ;>

}

Add a comment:

Nick:
URL (optional):
Math captcha: 1 ∗ 6 + 7 =