Lista blokowania wykonywania PHP

PHP_DENY_PATHS blokuje bezpośrednie wykonywanie plików .php pasujących do skonfigurowanych wzorców glob. Celuje w powracającą klasę podatności w starszych aplikacjach PHP: atakujący przesyła plik PHP do zapisywalnego katalogu publicznego (/uploads, /cache, katalogi tymczasowe do zmiany rozmiaru obrazów) i dociera do niego przez bezpośrednie URI, aby uzyskać wykonanie kodu.

Sprawdzenie odbywa się przed jakimikolwiek operacjami I/O na dysku, więc zablokowane ścieżki zwracają tę samą odpowiedź niezależnie od tego, czy plik istnieje na dysku, czy nie. Nie istnieje żadna wyrocznia istnienia, dzięki której atakujący mogliby badać katalogi przesyłania.

Kiedy ma zastosowanie

Tryby bezpośredniego mapowania, czyli tryby, w których URI odwzorowuje się wprost na plik .php na dysku:

Tryb routingu PHP_DENY_PATHS respektowane?
Tradycyjny (bez ENTRY_FILE) Tak
SPA (ENTRY_FILE=index.html) Tak — SPA wykonuje istniejące pliki .php bezpośrednio, więc lista blokowania ma zastosowanie
Framework (ENTRY_FILE=index.php) Nie — ostrzeżenie i zignorowanie
Worker (WORKER_MODE_ENABLED=true) Nie — ostrzeżenie i zignorowanie

W trybie Framework każde żądanie jest przepisywane do kontrolera frontowego i dowolne pliki .php nigdy nie są wykonywane bezpośrednio; lista blokowania psułaby jedynie trasy aplikacji, które przypadkiem kończą się na .php. W trybie Worker każde niestatyczne żądanie jest kierowane do skryptu workera, więc lista blokowania nie ma czego blokować. Ustawienie PHP_DENY_PATHS w którymkolwiek z tych trybów powoduje ostrzeżenie przy starcie i wyłącza sprawdzenie.

Lista blokowania obejmuje również skrypty osiągane pośrednio: żądanie do /uploads/, które odwzorowałoby się na uploads/index.php poprzez wyszukiwanie indeksu katalogu, jest blokowane, gdy uploads/** znajduje się na liście — do wzorców dopasowywana jest rozwiązana ścieżka skryptu, a nie tylko URI żądania. W przypadku takich blokad OXPHP_DENIED_PATH przenosi oczyszczone URI żądania bez końcowego ukośnika (/uploads/ jest raportowane jako /uploads).

Konfiguracja

bash
# Comma-separated glob patterns PHP_DENY_PATHS="/uploads/**,/cache/**,/tmp/**" # What to return on a match (default: 404) PHP_DENY_FALLBACK="403"

Żądanie do /uploads/shell.php zwraca teraz 403 bez dotykania dysku. Żądanie do /uploads/image.png jest obsługiwane normalnie — lista blokowania wpływa wyłącznie na wykonywanie .php, nigdy na serwowanie plików statycznych.

Składnia wzorców

Wzorce są dopasowywane do oczyszczonego URI (ścieżki żądania z już rozwiązanymi segmentami .. i obejściami zakodowanymi procentowo) przy użyciu składni globset. Wiodący / w każdym wzorcu jest opcjonalny — /uploads/** i uploads/** są równoważne.

Wzorzec Pasuje Nie pasuje
/uploads/** /uploads/x.php, /uploads/a/b/c.php, /uploads/shell.php/extra /uploads.php, /public/uploads/x.php
/files/*.php /files/x.php /files/sub/x.php (pojedyncza * nie przekracza /)
/admin/legacy.php /admin/legacy.php /admin/legacy.php/x (PATH_INFO nieobjęte — patrz niżej)
/admin/legacy.php{,/**} /admin/legacy.php, /admin/legacy.php/x /admin/other.php
/**/wp-config.php /wp-config.php, /site/wp-config.php /wp-config.txt

Wiele wzorców łączy się operatorem OR — żądanie pasuje do listy blokowania, jeśli pasuje do dowolnego wzorca.

Pojedyncze pliki a katalogi

Oba działają. /uploads/** blokuje całe poddrzewo; /admin/legacy.php blokuje jeden konkretny skrypt. Aby zablokować pojedynczy starszy punkt wejścia i wszelkie jego wywołania z PATH_INFO (/admin/legacy.php/foo), użyj formy z nawiasami klamrowymi: /admin/legacy.php{,/**}.

Rozróżnianie wielkości liter

Dopasowywanie rozróżnia wielkość liter. W systemach plików nierozróżniających wielkości liter (domyślne macOS HFS+/APFS, domyślne Windows NTFS, ext4 z casefold) żądanie do /uploads/Shell.PHP obeszłoby wzorzec /uploads/**/*.php. Podczas serwowania z takiego systemu plików użyj szerokiego wzorca katalogu, takiego jak /uploads/** (który pasuje do wszystkich rozszerzeń), albo normalizuj nazwy przesyłanych plików do małych liter w momencie zapisu.

Tryby awaryjne

PHP_DENY_FALLBACK kontroluje, co jest zwracane przy dopasowaniu.

Status HTTP

Dowolna wartość z zakresu 400599 (domyślnie 404). Łączy się z ERROR_PAGES_DIR w celu uzyskania niestandardowego ciała HTML:

bash
PHP_DENY_PATHS="/uploads/**" PHP_DENY_FALLBACK="403" ERROR_PAGES_DIR="/var/www/errors" # serves errors/403.html

Skrypt PHP

Ścieżka URI poprzedzona /, wskazująca skrypt awaryjny wewnątrz DOCUMENT_ROOT:

bash
PHP_DENY_PATHS="/uploads/**" PHP_DENY_FALLBACK="/_security/denied.php"

Skrypt jest walidowany przy starcie — musi istnieć, kanonikalizować się wewnątrz DOCUMENT_ROOT i sam nie może pasować do PHP_DENY_PATHS (zapobieganie pętli; w przeciwnym razie start jest przerywany). Skrypt działa z dwoma dodatkowymi kluczami $_SERVER identyfikującymi oryginalne żądanie:

Klucz $_SERVER Wartość
OXPHP_DENIED_PATH Oryginalne oczyszczone URI, z wiodącym / (ta sama forma co PATH_INFO)
OXPHP_DENIED_PATTERN Wzorzec glob, który dopasował

OXPHP_DENIED_PATTERN jest przechowywany bez wiodącego / (znormalizowany dla glob), podczas gdy OXPHP_DENIED_PATH zachowuje / z URI żądania. Jeśli porównujesz ścieżkę ze wzorcem, najpierw zastosuj ltrim($_SERVER['OXPHP_DENIED_PATH'], '/'), aby obie były w tej samej formie.

Przykładowa pułapka honeypot:

/_security/denied.php
<?php // /_security/denied.php — runs in place of any matched .php request. error_log(sprintf( "PHP execution denied: path=%s pattern=%s ip=%s ua=%s", $_SERVER['OXPHP_DENIED_PATH'] ?? '', $_SERVER['OXPHP_DENIED_PATTERN'] ?? '', $_SERVER['REMOTE_ADDR'] ?? '', $_SERVER['HTTP_USER_AGENT'] ?? '-', )); http_response_code(404); echo "Not Found";

Pozwala to zdecydować o odpowiedzi dla każdego żądania z osobna (zwrócić 404 atakującym, 403 uwierzytelnionym administratorom, przekierować skanery sondujące do sinkhole) zamiast ograniczać się do jednego statycznego statusu.

Brak wyroczni istnienia

Zarówno awaryjny Status, jak i Script są zwracane bez dotykania systemu plików. Żądanie do /uploads/never-uploaded.php oraz żądanie do /uploads/actually-on-disk.php dają identyczne odpowiedzi — brak różnicy w czasie, brak różnicy w ciele. Atakujący skanujący w poszukiwaniu przesłanych powłok nie może użyć listy blokowania do wyliczenia, które nazwy plików istnieją.

Ekran rozwiązanej ścieżki jest jedynym wyjątkiem: z konieczności działa po rozwiązaniu trasy, więc jego blokady zależą od istnienia. /uploads/ jest blokowane tylko wtedy, gdy uploads/index.php faktycznie istnieje na dysku; podobnie, przy wzorcu z pojedynczą gwiazdką, takim jak /uploads/*.php, żądanie z PATH_INFO /uploads/shell.php/x jest blokowane tylko wtedy, gdy uploads/shell.php istnieje (pełne URI nie pasuje do wzorca — pasuje rozwiązany skrypt). Ekran bezpośredniego URI — ten, który atakujący sondują — pozostaje wolny od wyroczni.

Obserwowalność

Metryka Opis
oxphp_php_deny_total Licznik zwiększany przy każdym zablokowanym żądaniu

Każda blokada generuje również log tracing::info:

text
PHP execution denied by PHP_DENY_PATHS path=uploads/shell.php pattern=uploads/**

Logi dostępu rejestrują wynikowy status (wartość PHP_DENY_FALLBACK lub http_response_code() skryptu awaryjnego) — zablokowane żądania nie są odróżniane od normalnych żądań na poziomie logu dostępu. Skoreluj z metryką lub logiem strukturalnym, aby przypisać skoki.

Wydajność

Dopasowywanie to wyszukiwanie globset::GlobSet — zazwyczaj pojedynczy przebieg SIMD po bajtach URI. Trafienie omija również pamięć podręczną tras (zablokowane URI pochodzą z kontrolowanego przez atakującego rozpylania o praktycznie nieograniczonej liczności; ich buforowanie pozwoliłoby atakującemu wypierać legalne wpisy z LRU). Zarówno ścieżka trafienia, jak i chybienia jest wolna od alokacji po rozgrzaniu.

Ograniczenia

Czego ta funkcja nie robi:

  • Obejście PATH_INFO dla wzorców literalnych plików. Wzorzec /admin/legacy.php nie pasuje do /admin/legacy.php/extra. Użyj /admin/legacy.php{,/**}, aby objąć oba, lub użyj wzorca katalogu.
  • Dopasowywanie rozróżniające wielkość liter (patrz Rozróżnianie wielkości liter powyżej).
  • Brak wyrażeń regularnych. Wzorce to wyłącznie globy — zakotwiczone, z operatorami */**/?/[abc]/{a,b}. Zamiast (a|b) użyj wielu wzorców oddzielonych przecinkami.
  • Brak wpływu na include / require / eval. Lista blokowania reguluje wyłącznie wykonywanie przez bezpośrednie URI. Podatny skrypt wykonujący include $_GET['page'] nadal może ładować PHP z dowolnego miejsca odczytywalnego przez serwer.

Przestarzały alias

Starsza zmienna PHP_DENY_DIRS jest akceptowana jako przestarzały alias i generuje ostrzeżenie przy starcie:

text
WARN PHP_DENY_DIRS is deprecated, use PHP_DENY_PATHS instead — the alias will be removed in a future release

Gdy obie są ustawione, PHP_DENY_PATHS wygrywa, a PHP_DENY_DIRS jest raportowane jako zignorowane. Wartości nie są scalane.

Zobacz też