Dane dotyczące stanu środowiska znajdują się w superglobalnej tablicy $_SERVER. Wiele skryptów, w tym także i mój, korzysta z elementu $_SERVER['SERVER_NAME'] do określenia nazwy domeny, w obrębie której został on uruchomiony. W moim przypadku od niej zależało wybrane środowisko, a później także postać adresów URL. Okazało się, że skrypt dostaje w tym polu zupełnie inne informacje, niż przewidywał. Wywoływałem demoapp1.scriptist.lh, lecz tam miałem ciągle localhost. Wyświetlenie pełnej zawartości tablicy ujawniło, że oczekiwane dane także są dostarczane, lecz w polu $_SERVER['HTTP_HOST']. Sprawa mnie zaintrygowała, dlatego znalazłem w sieci więcej informacji i tak trafiłem tutaj.
Pole HTTP_HOST odpowiada dokładnie zawartości nagłówka "Host:" w żądaniu HTTP. Jeżeli takiego nagłówka nie ma, pole przyjmuje pustą wartość. Inaczej sprawa ma się z SERVER_NAME. Może ono bazować na zawartości wspomnianego nagłówka, ale równie dobrze może tam znaleźć się domyślny host serwera lub też zawartość nagłówka zostanie poddana modyfikacjom. Autor załączonego wyżej linka przeprowadził programem telnet kilka testów, aby sprawdzić zachowanie się obu tych pól. Polegało to na umieszczeniu na serwerze w głównym katalogu skryptu o treści:
<?php echo 'HTTP_HOST: ['.$_SERVER['HTTP_HOST']."]\n"; echo 'SERVER_NAME: ['.$_SERVER['SERVER_NAME']."]\n"; ?>
Następnie telnetem wysyłamy żądanie HTTP podobne do tego poniżej:
# telnet 127.0.0.1 80 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. GET /http.php HTTP/1.0 Host: localhost
Serwer odpowie wynikiem działania skryptu. Autor przetestował kilka konfiguracji - bez nagłówka host oraz z różnymi jego wartościami. Powtórzyłem go u siebie na dwóch konfiguracjach: Lighttpd+FastCGI oraz Apache+mod_php.
| Żądanie | Odpowiedź |
GET /http.php HTTP/1.0 |
403 Forbidden :) |
GET /http.php HTTP/1.0 Host: localhost |
HTTP_HOST: [localhost] SERVER_NAME: [localhost] |
GET /http.php HTTP/1.0 Host: test.sites.lh |
HTTP_HOST: [test.sites.lh] SERVER_NAME: [localhost] |
GET /http.php HTTP/1.0 Host: <xss>--atak |
400 Bad Request |
Lighttpd okazał się dość idiotoodporny. Żądanie bez nagłówka zawsze powoduje wygenerowanie błędu 403 (choć możliwe, że po prostu wyszła mi taka konfiguracja - nie znam jeszcze dobrze tego programu), zaś próba ataku XSS została udaremniona przez komunikat o błędnym żądaniu HTTP. Co ciekawe, przy łączeniu się z istniejącym hostem, w SERVER_NAME pozostawała nazwa "localhost" i właśnie to było przyczyną niedziałania mojego silnika. Testy powtórzyłem dla Apache+mod_php:
| Żądanie | Odpowiedź |
GET /http.php HTTP/1.0 |
HTTP_HOST: [] SERVER_NAME: [localhost] |
GET /http.php HTTP/1.0 Host: localhost |
HTTP_HOST: [localhost] SERVER_NAME: [localhost] |
GET /http.php HTTP/1.0 Host: test.sites.lh |
HTTP_HOST: [test.sites.lh] SERVER_NAME: [test.sites.lh] |
GET /http.php HTTP/1.0 Host: <xss>--atak |
HTTP_HOST: [<xss>--atak] SERVER_NAME: [<xss>--atak] |
Wyniki są zgoła odmienne. Przede wszystkim nazwa użytego hosta jest zawsze przepisywana do SERVER_NAME, o ile została ustawiona, a co więcej - poddawana jest escapingowi. Zauważmy, że w przypadku próby ataku XSS w HTTP_HOST cała nazwa pozostała w oryginalnej postaci, zaś w drugim została przefiltrowana, dzięki czemu nie stanowi zagrożenia. Co prawda wykonany tutaj atak XSS jest trochę bez sensu, bo właściwie atakuję sam siebie, ale nie wolno lekceważyć pomysłowości włamywaczy. Być może ktoś, komu by zależało, zrobiłby z niej właściwy użytek.
Można stąd wysnuć kilka wniosków. Korzystanie z surowych danych dostarczonych przez tablicę $_SERVER rodzi bardzo dużo problemów, ponieważ uzależniamy nasz skrypt od jednej, konkretnej platformy. Porządna aplikacja powinna być zaopatrzona w odpowiedni filtr, który by analizował zawartość wspomnianej tablicy i na jej podstawie dopiero budował użyteczne dla skryptu dane. Co więcej, danym tym nie powinno się bezkrytycznie ufać. Część z nich pochodzi bezpośrednio z nagłówków żądania HTTP, zatem może być dowolnie zmanipulowana przez sprytnych włamywaczy. Prawdziwie bezpiecznych pól jest zaledwie parę (np. REMOTE_ADDR) i choć zawarte w nich informacje są ograniczone, można na nich spokojnie polegać, zwłaszcza tam, gdzie w grę wchodzą sprawy związane z dostępem i bezpieczeństwem.















Napisał Bob w czwartek, 14 lutego 2008 o 15:33
Przecież REMOTE_ADDR też może być dowolny np. 127.0.0.1 przy dobrym proxy.