Krótko: Local File Inclusion w PHP powstaje, gdy include, require lub funkcje plikowe używają ścieżki, na którą może wpływać odwiedzający. Bezpiecznym rozwiązaniem jest stała lista dozwolonych: parametr wybiera tylko spośród znanych wartości, a sama ścieżka nigdy nie pochodzi z żądania. Po stronie serwera ochronę uzupełniają allow_url_include=Off, open_basedir i restrykcyjne uprawnienia do plików.
Czym jest Local File Inclusion w PHP?
Wiele starszych aplikacji PHP ładuje strony według prostego wzorca: parametr w adresie określa, który plik zostanie dołączony. Jeśli ten parametr jest przekazywany niesprawdzony do include, atakujący może żądać innych plików na serwerze. W łagodniejszym przypadku odczytuje pliki konfiguracyjne z danymi dostępowymi. W gorszym nakłania PHP do wykonania treści, które wcześniej umieścił gdzie indziej, na przykład w uploadzie lub pliku logu.
Dla zarządu ważne jest: ta luka rzadko tkwi w rdzeniu CMS, a zwykle w rozwiązaniach własnych, starych rozszerzeniach lub szybko zbudowanych stronach dodatkowych. Na co dzień nie rzuca się w oczy, ponieważ strona działa normalnie.
W przypadku Remote File Inclusion PHP ładuje kod z obcego adresu. Zakłada to, że allow_url_include jest włączone. W aktualnych wersjach PHP to ustawienie jest domyślnie wyłączone, ale na starych lub błędnie skonfigurowanych serwerach nadal się je spotyka.
Typowe podatne wzorce w kodzie
Zagrożone są miejsca, w których wartość z żądania, z ciasteczek lub z bazy danych staje się częścią ścieżki pliku. Częste przykłady to przełączniki języka ładujące plik językowy według parametru, wybór stron przez parametr w adresie, wybór szablonu lub motywu oraz skrypty pobierania dokumentów. Dołączone na końcu ścieżki .php nie chroni tu niezawodnie, ponieważ atakujący mogą przejść na istniejące pliki PHP.
Pierwszy przegląd uzyskają Państwo, wyszukując dołączenia zawierające zmienne. Daje to także nieszkodliwe trafienia, ale pokazuje, gdzie trzeba przyjrzeć się dokładniej:
grep -rnE --include='*.php' '(include|require)(_once)?\s*\(?\s*\$' /var/www/projekt \
| grep -v '/vendor/'
Każde trafienie proszę sprawdzić pod kątem tego, skąd pochodzi zmienna i czy odwiedzający może na nią wpływać.
Bezpieczny wzorzec: lista dozwolonych zamiast ścieżki z żądania
Najpewniejszą ochroną jest to, że odwiedzający wybiera tylko klucz, a aplikacja sama decyduje, który plik do niego należy. Ścieżka nigdy nie jest składana z danych wejściowych.
<?php
declare(strict_types=1);
$seiten = [
'start' => __DIR__ . '/../vorlagen/start.php',
'leistungen' => __DIR__ . '/../vorlagen/leistungen.php',
'kontakt' => __DIR__ . '/../vorlagen/kontakt.php',
];
$wahl = is_string($_GET['seite'] ?? null) ? $_GET['seite'] : 'start';
if (!array_key_exists($wahl, $seiten)) {
http_response_code(404);
$wahl = 'start';
}
require $seiten[$wahl];
Ten wzorzec jest krótki, łatwy do sprawdzenia i nie zostawia pola manewru. W nowoczesnych frameworkach to zadanie przejmuje router; tam kontrolery i szablony również nigdy nie powinny być określane z wolnych parametrów.
Gdy nazwy plików muszą być dynamiczne: realpath i katalog bazowy
Czasem stała lista jest niepraktyczna, na przykład przy pobieraniach z folderu dokumentów. Wtedy sprawdzają Państwo rozwiązaną ścieżkę względem stałego katalogu bazowego i dopuszczają tylko proste nazwy plików.
<?php
declare(strict_types=1);
const BASIS = '/var/www/daten/dokumente';
function sichererPfad(string $name): string
{
if (!preg_match('/^[a-z0-9_-]{1,64}\.pdf$/', $name)) {
throw new InvalidArgumentException('Ungültiger Dateiname.');
}
$basis = realpath(BASIS);
$pfad = realpath($basis . DIRECTORY_SEPARATOR . $name);
if ($basis === false || $pfad === false
|| !str_starts_with($pfad, $basis . DIRECTORY_SEPARATOR)) {
throw new RuntimeException('Datei nicht gefunden.');
}
return $pfad;
}
// Udostępniać zamiast dołączać: readfile() nie wykonuje kodu.
$datei = sichererPfad((string) ($_GET['datei'] ?? ''));
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . basename($datei) . '"');
readfile($datei);
Rozstrzygające są dwa punkty. Wyrażenie regularne nie dopuszcza separatorów katalogów ani znaków specjalnych. A pliki, które mają być tylko udostępniane, czyta się przez readfile(), a nie przez include, aby zawarty kod nigdy nie został wykonany. str_starts_with() jest dostępne od PHP 8.0.
Zabezpieczenie po stronie serwera: php.ini, open_basedir i uprawnienia
Bezpieczny kod to pierwsza linia obrony. Konfiguracja serwera sprawia, że przeoczony błąd wyrządza mniej szkody.
; php.ini lub pula FPM (php_admin_value / php_admin_flag)
allow_url_include = Off
allow_url_fopen = Off ; tylko jeśli aplikacja nie ładuje zewnętrznych adresów URL
open_basedir = /var/www/kunde-a/:/tmp/kunde-a/
display_errors = Off
log_errors = On
allow_url_fopen jest potrzebne niektórym aplikacjom do aktualizacji lub interfejsów. Wyłączenie proszę najpierw przetestować w środowisku testowym. open_basedir ogranicza PHP do własnych katalogów strony, co jest szczególnie ważne na serwerach z wieloma klientami. Szczegóły znajdują się w poradnikach open_basedir i izolacja każdej strony oraz Jak bezpiecznie skonfigurować php.ini.
Uzupełniająco foldery uploadu i pamięci podręcznej nie powinny być wykonywalne, a pliki logów nie powinny leżeć w webrootcie. Atakującemu brakuje wtedy miejsc, w których mógłby umieścić własną zawartość do dołączenia.
Po czym poznać próby wykorzystania w logach
Próby wykorzystania file inclusion zostawiają zwykle ślady w logu dostępu. Podejrzane są parametry zawierające zmiany katalogów, bezwzględne ścieżki systemowe, wrappery strumieni lub pełne obce adresy, zwłaszcza w parametrach takich jak page, file, template czy lang. Pojedyncze trafienie to zwykle zautomatyzowane skanowanie. Ważne jest, czy na takie żądanie odpowiedziano kodem 200 i nietypowym rozmiarem odpowiedzi.
Jak postępować przy tym systematycznie, opisuje poradnik Analiza logów serwera WWW po ataku. Jeśli znajdą Państwo wskazówki na udane dostępy, proszę założyć, że pliki konfiguracyjne zostały odczytane, i odnowić zawarte w nich dane dostępowe.
Należą do nich przede wszystkim hasła do bazy danych, klucze do usług płatności i wysyłki, dostępy SMTP oraz tajne klucze CMS. Proszę sprawdzić także, czy od pierwszego podejrzanego dostępu powstały nowe pliki. Kto mógł czytać pliki, być może próbował też zapisywać własne. W takim przypadku nie chodzi już tylko o lukę w kodzie, ale o incydent bezpieczeństwa, który należy obsłużyć w sposób uporządkowany.
Trwałe zabezpieczanie poprawek
Po poprawce warto zabezpieczyć funkcję ochronną zautomatyzowanymi testami. Prosty test przekazuje wyborowi stron nieznane wartości, zmiany katalogów i puste dane i oczekuje we wszystkich przypadkach strony domyślnej lub błędu 404. Takie testy zapobiegają temu, by późniejsza zmiana niepostrzeżenie znów otworzyła lukę.
Dla zarządu i osób odpowiedzialnych za projekt pomocne jest pytanie do agencji: czy w aplikacji są miejsca, w których pliki są ładowane według parametrów, i jak są zabezpieczone? Konkretna odpowiedź ze wskazaniem miejsc i testów jest dobrym znakiem.
Jak konkretnie pomaga PHP SOS?
- Szukamy w Państwa kodzie celowo
include,requirei funkcji plikowych z danymi z żądania i oceniamy każde znalezisko. - Ustawiamy
allow_url_include,open_basediri odpowiednie uprawnienia dla każdej strony i sprawdzamy, czy aplikacja po tym nadal działa. - Analizujemy logi pod kątem prób wykorzystania i wyjaśniamy, czy dane dostępowe trzeba odnowić.
- Dla niezależnej kontroli oferujemy audyt bezpieczeństwa z testem penetracyjnym aplikacji PHP.
Częste pytania
Jaka jest różnica między LFI a RFI?
Przy Local File Inclusion aplikacja ładuje plik, który już leży na serwerze, ale nie powinien być ładowany. Przy Remote File Inclusion kod jest doładowywany z obcego adresu. RFI wymaga niebezpiecznego ustawienia PHP, LFI nie.
Czy wystarczy usunąć ../ z parametru?
Nie. Takie filtry często można obejść przez kodowanie lub warianty i pomijają ścieżki bezwzględne. Bezpieczne jest tylko stałe przyporządkowanie dozwolonych wartości do plików lub sprawdzenie rozwiązanej ścieżki względem katalogu bazowego.
Czy open_basedir w pełni chroni przed file inclusion?
Nie, ogranicza tylko katalogi, do których PHP ma dostęp. W obrębie tych granic luka pozostaje możliwa do wykorzystania. open_basedir jest dodatkową warstwą ochrony i nie zastępuje bezpiecznego kodu.
Czy systemy CMS takie jak WordPress są dotknięte LFI?
Rdzeń popularnych systemów CMS uchodzi pod tym względem za dobrze sprawdzony. Luki pojawiają się raczej w rozszerzeniach, motywach i rozwiązaniach własnych. Aktualizacje rozszerzeń i spojrzenie na ich komunikaty bezpieczeństwa są więc ważne.