Reguły blokowania ścieżek (.oxphpdeny)

Plik .oxphpdeny na najwyższym poziomie DOCUMENT_ROOT wymienia ścieżki żądań, których serwer nie może serwować bezpośrednio. Zapisuje się go w składni .gitignore z jednym dodatkiem: reguła poprzedzona > nie blokuje żądania, lecz przekazuje je do skryptu wejściowego aplikacji.

.oxphpdeny
# Never served — answered with PHP_DENY_FALLBACK (404 by default) /vendor /composer.* *.sql *.bak # Not served directly — the front controller decides > /storage/invoices/

Jest przeznaczony dla plików, których katalog główny dokumentów nie powinien zawierać, a często zawiera: vendor/ i composer.json w starszej aplikacji serwowanej z katalogu głównego projektu, zrzuty baz danych i kopie zapasowe, artefakty budowania i mapy źródeł w katalogu public/ frameworka. Reguły obowiązują we wszystkich czterech trybach routingu, zarówno dla plików statycznych, jak i dla skryptów PHP. Są sprawdzane przed jakimkolwiek dostępem do dysku, więc zablokowana ścieżka otrzymuje tę samą odpowiedź niezależnie od tego, czy plik istnieje.

Note

Plik jest odczytywany raz, przy starcie: zmiany zaczynają obowiązywać przy następnym uruchomieniu. Bez pliku nic się nie zmienia. Sam plik nigdy nie jest serwowany. Jak każdy segment ścieżki zaczynający się od ., poza wiodącym /.well-known/, jest odrzucany przed routingiem przez blokowanie ścieżek z kropką.

Składnia

Wszystko, co obsługuje .gitignore, obsługuje też .oxphpdeny:

  • Puste linie i linie zaczynające się od # są pomijane. Akceptowane są znacznik kolejności bajtów UTF-8 (BOM) i końce linii CRLF.
  • Końcowe spacje są usuwane, chyba że poprzedza je znak ucieczki (\ ).
  • ! ponownie włącza to, co wykluczyła wcześniejsza reguła. \# i \! na początku linii oznaczają dosłowne # i !.
  • Końcowy / sprawia, że reguła dopasowuje wyłącznie katalogi.
  • / na początku lub w środku zakotwicza wzorzec na najwyższym poziomie DOCUMENT_ROOT. Bez niego wzorzec pasuje na dowolnej głębokości: vendor obejmuje /vendor i /lib/vendor.
  • * i ? nie wychodzą poza jeden segment ścieżki; [abc], [a-z], [!0-9] i [^0-9] to klasy znaków. Tak jak w git, ? i klasa dopasowują pojedynczy bajt, a klasa nigdy nie dopasowuje /: [!-] to dowolny bajt nazwy poza -. Litera spoza ASCII zajmuje dwa lub więcej bajtów, więc zastąpić ją może tylko *: pisz /caf*.txt, a nie /caf?.txt. Wewnątrz klasy nie są obsługiwane klasy POSIX, sekwencje ucieczki z ukośnikiem wstecznym, znaki spoza ASCII, - tuż po zakresie ani zakres zapisany odwrotnie, a zanegowana klasa w regule blokującej lub regule > nie może wymieniać litery ASCII (zobacz Różnice względem .gitignore).
  • **/ na początku, /** na końcu i /**/ w środku dopasowują przez wiele katalogów; ** w każdym innym miejscu to zwykła *. Dłuższy ciąg * liczy się jako **, tak jak w git: ***/secret.txt obejmuje /secret.txt na dowolnej głębokości.
  • Wygrywa ostatnia pasująca reguła i niczego poniżej wykluczonego katalogu nie da się ponownie włączyć: przy wykluczonym private/ reguła !private/ok.txt nie ma żadnego efektu. Aby wykluczyć zawartość katalogu, ale zachować jej część, wyklucz /private/* i ponownie włącz !/private/ok.txt.

Reguły >: przekazanie żądania aplikacji

.oxphpdeny
> /storage/invoices/ > *.map

Żądanie pasujące do reguły > nie jest serwowane z dysku. Trafia do skryptu wejściowego z nienaruszonym REQUEST_URI, dokładnie tak jak żądanie brakującego pliku, a aplikacja może sprawdzić sesję i sama przesłać plik strumieniowo. Skryptem wejściowym jest kontroler frontowy (ENTRY_FILE=index.php) albo worker. Spacje i tabulatory między > a wzorcem są dozwolone; \> oznacza dosłowne >. Reguły > nie da się zanegować (!>x to błąd), a samo > również jest błędem.

Reguła blokująca nadal obowiązuje poniżej katalogu >: przy > /storage/invoices/ i *.sql ścieżka /storage/invoices/dump.sql jest blokowana, niezależnie od tego, która linia jest pierwsza. Reguła !, która znosi blokadę, zostawia plik katalogowi >, tak jak w git: dodaj !/storage/invoices/dump.sql po *.sql, a to żądanie trafi do skryptu wejściowego. ! niczego nie włącza ponownie poniżej katalogu >, tak jak poniżej każdego wykluczonego katalogu, więc plik nadal nie jest serwowany z dysku.

Warning

Reguły > wymagają skryptu wejściowego. W trybach Tradycyjnym i SPA przerywają start.

Różnice względem .gitignore

  • Błędnie sformułowana linia przerywa start, z podaniem ścieżki pliku i numeru linii; podobnie plik, który nie jest zapisany w UTF-8. git po cichu pomija takie linie.
  • {a,b} jest dosłowne. Nawiasy klamrowe pasują same do siebie, tak jak w git.
  • Klasa wymieniająca / przerywa start, czy to bezpośrednio ([-/]), czy przez obejmujący go zakres, taki jak [+-0]. git ją akceptuje i nigdy nie dopasowuje nią /; zapisz separator poza klasą.
  • Klasa POSIX, ukośnik wsteczny wewnątrz klasy albo - tuż po zakresie przerywa start: [[:digit:]], [\]], [a-b-c]. git odczytuje każdy z nich na swój sposób. Zamiast tego zapisz zakres ([0-9]), umieść ] na początku, aby go dopasować, a dosłowny - na początku lub na końcu ([]-], [-a-c]).
  • Zakres zapisany odwrotnie przerywa start, na przykład [z-a] lub [a--]. git odczytuje go jako sam pierwszy znak, więc x[z-a] obejmuje tylko xz; zapisz ten znak albo umieść dolną granicę na początku.
  • Klasa zawierająca znak spoza ASCII przerywa start, na przykład [äÄ]. git dopasowuje klasę do jednego bajtu, tak jak serwer, a taki znak zajmuje dwa lub więcej: /[äÄ]dmin nie objąłby ani /ädmin, ani /Ädmin, a /[äÄ]* objąłby /öffnungszeiten, które zaczyna się od tego samego bajtu. Umieść w jego miejscu *.
  • Ukośnik / poprzedzony znakiem ucieczki przerywa start (**\/secret.txt, \/vendor). git odczytuje **\/secret.txt jako secret.txt poniżej co najmniej jednego katalogu (nie samo /secret.txt), czego serwer by nie zrobił, a \/vendor nie pasuje do żadnej ścieżki. Separator nigdy nie wymaga znaku ucieczki: pisz /.
  • Linia, która obejmuje coś innego, niż wynika z jej zapisu, przerywa start, czyli taka, która zaczyna się od spacji lub tabulatora, kończy się tabulatorem bez znaku ucieczki, zawiera gdziekolwiek // albo zawiera bajt NUL. git akceptuje każdą z nich. Białe znaki na którymkolwiek końcu stają się częścią nazwy: wcięta linia > /storage/invoices/ to reguła blokująca dla ścieżki zaczynającej się od spacji, a nie reguła >. Poprzedź je znakiem \, gdy nazwa naprawdę się od nich zaczyna lub na nich kończy. //vendor i vendor// nie pasują do żadnej ścieżki. git kończy wzorzec na bajcie NUL, więc vendor, NUL i dalszy tekst obejmują w git /vendor, podczas gdy serwer odczytałby całą linię i objąłby tylko nazwę zawierającą ten NUL. Plik zapisany w UTF-16 ma NUL po każdym znaku ASCII: zapisz go w UTF-8.
  • Linia złożona ze spacji i tabulatorów jest pusta. git odczytuje linię zawierającą tabulator jako wzorzec dla nazwy złożonej z tych białych znaków.
  • Reguły blokujące i reguły > ignorują wielkość liter ASCII; reguły ! nie. Serwer uruchamia /uploads/shell.PHP jako skrypt PHP w każdym systemie plików, a system plików nierozróżniający wielkości liter (macOS, Windows) serwuje /VENDOR/autoload.php z vendor/, więc reguła dopasowująca jedną pisownię przepuszczałaby pozostałe: *.php obejmuje shell.PHP, a vendor/ obejmuje /Vendor/. Reguła ! ponownie włącza tylko pisownię, którą wymienia (przy * i !/index.php ścieżka /INDEX.PHP pozostaje zablokowana), więc przez ignorowanie wielkości liter serwer nigdy nie zaserwuje z dysku ścieżki, którą dokładne dopasowanie by przed nim zablokowało. Żeby tak zostało, zanegowana klasa w regule blokującej lub regule > nie może wymieniać litery ASCII, a klasa, która to robi, przerywa start: przy ignorowaniu wielkości liter [!i] pomijałoby także I, a przy /[!i]*.php serwer uruchomiłby /Info.php. Dopasuj w tym miejscu dowolny znak i ponownie włącz wyjątek, który reguła ! dopasuje wtedy dokładnie: /*.php i !/i*.php. git dopasowuje z rozróżnianiem wielkości liter, chyba że ustawiono core.ignorecase.
  • Kilka liter spoza ASCII jest odczytywanych jako ASCII. Tak odczytuje je system plików nierozróżniający wielkości liter (macOS), który dla /licenſe.txt serwuje license.txt: ſ (długie s) jako s, ß i ẞ jako ss, symbol kelwina (U+212A) jako k, ligatury ff fi fl ffi ffl ſt st jako litery, które łączą, a grecki znak zapytania (U+037E) i grecką varię (U+1FEF) jako ; i `. Reguły blokujące i reguły > odczytują je tak samo, zarówno w żądaniu, jak i w regule: /license.txt obejmuje /licenſe.txt, a /straße/ obejmuje /strasse/. Reguła nadal obejmuje to, co obejmowała w zapisanej postaci, a reguły ! odczytują te litery tylko tak, jak zostały zapisane, więc blokada może się jedynie poszerzyć.
  • Pozostałe litery spoza ASCII pasują tylko w zapisanej postaci, podobnie jak każda postać Unicode nazwy. Reguła dla /ädmin nie obejmuje /Ädmin, a reguła dla café.txt zapisana ze złożonym é nie obejmuje cafe z akcentem łączącym (U+0301). Większość linuksowych systemów plików przechowuje każdą pisownię jako osobny plik, więc ma to znaczenie w macOS, który traktuje obie pary jako jeden plik, także przez montowanie bind z macOS do Docker Desktop. Tam żądanie o inną pisownię jest serwowane z dysku. Umieść * w miejscu takich liter (/caf*.txt), ponieważ ? i klasa dopasowują pojedynczy bajt, albo trzymaj te pliki poza DOCUMENT_ROOT.
  • Najwyżej 256 reguł, każda najwyżej 128 bajtów. Reguła przekraczająca którykolwiek z tych limitów przerywa start z podaniem numeru linii (zobacz Limity).
  • Jeden plik, na najwyższym poziomie DOCUMENT_ROOT, odczytywany przy starcie. Pliki w podkatalogach nie są odczytywane. Musi to być zwykły plik o rozmiarze najwyżej 1 MiB; dowiązanie symboliczne do takiego pliku jest w porządku. FIFO lub urządzenie przerywa start bez odczytu, a większy plik przerywa go po odczytaniu pierwszego MiB.

Co jest dopasowywane

Reguły dopasowują ścieżkę żądania po dekodowaniu procentowym i normalizacji. Puste segmenty są pomijane, więc //vendor/x, /vend%6Fr/x i /vendor%2Fx to za każdym razem /vendor/x. Segment zaczynający się od . (., .., .git) nigdy nie dociera do reguł: żądanie zostaje wcześniej odrzucone zwykłym 404, bez PHP_DENY_FALLBACK i bez zliczania. Wyjątkiem jest wiodące /.well-known/ (RFC 8615), które blokowanie ścieżek z kropką przepuszcza, a reguły widzą jak każdą inną ścieżkę.

Żądanie jest żądaniem katalogu, gdy jego zdekodowana ścieżka kończy się na / (/admin/, /admin%2F, /admin//). Reguła z końcowym ukośnikiem dopasowuje tylko takie żądania i wszystko poniżej nich:

Reguła /admin /admin/ /admin/x /admin/x/y
admin/ — zablokowane zablokowane zablokowane
admin lub /admin zablokowane zablokowane zablokowane zablokowane
admin/*, admin/x — — zablokowane zablokowane

Samo / nigdy nie jest dopasowywane, więc plik w stylu listy dozwolonych nadal serwuje katalog główny witryny:

.oxphpdeny
* !/index.php !/assets/ !/assets/** !/.well-known/ !/.well-known/**
ACME HTTP-01

Zachowaj linie /.well-known/, jeśli witryna odnawia certyfikat TLS przez ACME HTTP-01. Bez nich /.well-known/acme-challenge/<token> jest blokowane.

Ocenie podlegają też dwa inne odczytania żądania, a blokada wynikająca z któregokolwiek z nich obowiązuje:

  • Żądanie katalogu jest oceniane także jako plik, który nazywa. Router usuwa końcowy ukośnik i może zaserwować /secret.txt/ jako plik secret.txt.
  • W trybie Tradycyjnym segment .php, po którym następuje dalsza część ścieżki, jest oceniany jako skrypt, który nazywa. /secret.php/x.js uruchamia secret.php z PATH_INFO /x.js, więc jest blokowane wszędzie tam, gdzie /secret.php.

Tak czy inaczej !*/, czyli sposób gitignore na ponowne włączenie każdego katalogu, nie może przywrócić zablokowanego pliku. Na liście dozwolonych włącz ponownie katalog bez ukośnika (!/docs), aby samo /docs/ pozostało osiągalne. Żaden inny plik, na który router rozwiązuje żądanie, nie jest oceniany, co pokazuje następna sekcja.

Ścieżka żądania, nie plik

Reguły widzą ścieżkę, o którą poprosił klient, a nie plik, na który router ją rozwiązuje. Żądanie rozwiązywane na inny plik jest oceniane według własnej ścieżki:

  • /admin z regułą admin/: w trybie Tradycyjnym router odpowiada na /admin plikiem admin/index.php, a /admin nie jest żądaniem katalogu.
  • /uploads/ z regułą uploads/*.php: uruchamia się indeks katalogu uploads/index.php, ponieważ ścieżka żądania to /uploads/.
  • /x.php/y.png (PATH_INFO) uruchamia x.php, choć ścieżka kończy się na .png. Skrypt jest nazwany w ścieżce, więc tryb Tradycyjny ocenia żądanie także jako /x.php, jak opisano wyżej.
Tip

Aby zamknąć katalog, zapisz go bez końcowego ukośnika (/admin). Aby zatrzymać wykonywanie .php w katalogach zapisywalnych, użyj dodatkowo PHP_DENY_PATHS: sprawdza ono również skrypt, na który rozwiązuje się żądanie.

Tryby routingu

Tryb routingu Reguły blokujące Reguły >
Tradycyjny (bez ENTRY_FILE) Tak Błąd startu: nie ma skryptu wejściowego
SPA (ENTRY_FILE=index.html) Tak Błąd startu, ponieważ index.html nie może przyjąć żądania
Framework (ENTRY_FILE=index.php) Tak Kontroler frontowy
Worker (WORKER_MODE_ENABLED=true) Tak, z odpowiedzią awaryjną w postaci kodu statusu Punkt wejścia workera

Odpowiedź awaryjna

Reguła blokująca odpowiada zgodnie z PHP_DENY_FALLBACK, ustawieniem, którego używa PHP_DENY_PATHS: statusem HTTP (domyślnie 404; łączy się z ERROR_PAGES_DIR) albo skryptem PHP wewnątrz DOCUMENT_ROOT. Skrypt otrzymuje OXPHP_DENIED_PATH (ścieżkę żądania z wiodącym /) oraz OXPHP_DENIED_PATTERN (regułę w postaci zapisanej w pliku, np. /composer.*).

Skrypt może znajdować się pod ścieżką, którą plik blokuje. /_security/ ukrywa go przed bezpośrednimi żądaniami, a na bezpośrednie żądanie do niego odpowiada sam skrypt, tak jak na blokadę. PHP_DENY_FALLBACK jest odczytywane tylko wtedy, gdy plik zawiera co najmniej jedną regułę blokującą (lub gdy działa PHP_DENY_PATHS, w trybach Tradycyjnym i SPA).

Note

W trybie Worker odpowiedzią awaryjną musi być kod statusu: wątek workera nie uruchamia niczego poza punktem wejścia workera.

Gdy do żądania pasują zarówno .oxphpdeny, jak i PHP_DENY_PATHS, odpowiada .oxphpdeny.

Obserwowalność

  • Metryka oxphp_path_deny_total zlicza żądania zablokowane przez regułę. Reguły > to routing, a nie blokady, więc nie są zliczane. oxphp_php_deny_total nadal zlicza wyłącznie PHP_DENY_PATHS; żądanie zablokowane przez oba mechanizmy jest liczone raz, w oxphp_path_deny_total.
  • Na /config wpis deny_file podaje liczby reguł według akcji i rodzaj odpowiedzi awaryjnej, nigdy same reguły (zobacz Serwer wewnętrzny).
  • Log startowy raportuje loaded 5 rules (3 deny, 1 entry, 1 allow) from /var/www/html/public/.oxphpdeny. Przy LOG_LEVEL=debug dla każdej reguły pojawia się linia oxphpdeny rule, która pokazuje, jak reguła została odczytana: numer linii, akcję, wzorzec w zapisanej postaci i glob, do którego go skompilowano.
  • Dla każdego żądania każda blokada loguje request denied by .oxphpdeny na poziomie debug, ze ścieżką, regułą i numerem jej linii. Żądanie odrzucone z powodu głębokości lub długości loguje request refused: path too deep or too long to match against .oxphpdeny na poziomie debug, z liczbą segmentów i długością; nie jest ono zliczane.

Błędy startu

Warunek Wynik
Brak .oxphpdeny Funkcja wyłączona; nic się nie zmienia
Plik nieczytelny lub niezapisany w UTF-8 albo dowiązanie symboliczne, którego cel nie istnieje Błąd startu
Błędnie sformułowana linia Błąd startu z <path>: line N: <message>
Więcej niż 256 reguł lub wzorzec dłuższy niż 128 bajtów Błąd startu z numerem linii
Reguła > w trybie Tradycyjnym lub SPA Błąd startu
Tryb Worker, reguła blokująca i skrypt jako PHP_DENY_FALLBACK Błąd startu
Nieprawidłowe PHP_DENY_FALLBACK i co najmniej jedna reguła blokująca Błąd startu
Pusty plik, same komentarze lub tylko reguły ! Nie ma czego blokować: routing pozostaje bez zmian, łącznie z limitem głębokości; /config pokazuje plik jako załadowany wraz z liczbami reguł

oxphp config --check zgłasza błędy pliku przed wdrożeniem (config: INVALID). Sprawdzenia odpowiedzi awaryjnej wykonywane są przy starcie, gdy serwer buduje swój routing.

Limity

  • Głębokość i długość ścieżki są ograniczone. Przy co najmniej jednej regule blokującej lub regule > ścieżka żądania głębsza niż 64 segmenty albo taka, której liczba segmentów pomnożona przez długość przekracza 128 KiB, otrzymuje odpowiedź 404 bez dopasowywania. Dopasowanie ponownie sprawdza każdy katalog nadrzędny i skanuje ścieżkę każdego z nich, więc w przeciwnym razie jego koszt rósłby wraz z obiema tymi wielkościami. Ścieżka o długości do 2 KiB jest odrzucana wyłącznie z powodu głębokości; powyżej tej długości budżet wyczerpuje się przy 16 segmentach dla 8 KiB albo przy 3 dla około 43 KiB.
  • Plik zawiera najwyżej 256 reguł, a każdy wzorzec ma najwyżej 128 bajtów, nie licząc wiodącego ! ani >. Ścieżka żądania jest dopasowywana do reguł po kolei, a także do każdego katalogu nadrzędnego, więc te limity i limit opisany powyżej ograniczają koszt żądania. Typowy plik zajmuje znacznie mniej niż milisekundę na żądanie. Plik na obu limitach, zbudowany tak, by był wolny (256 długich reguł zaczynających się od /x/**/*, wobec ścieżek spreparowanych tak, by prawie do nich pasowały), zajmuje około 40 ms na żądanie i utrzymuje około 50 MB stanu dopasowania na każdy wątek roboczy Tokio.
  • Rozróżnianie wielkości liter opisuje sekcja Różnice względem .gitignore.
  • Nie ma przeładowania. Aby zastosować zmianę, uruchom serwer ponownie.

Przykłady

Starsza aplikacja serwowana z katalogu głównego projektu:

.oxphpdeny
/vendor /composer.* /config *.sql *.sql.gz *.bak *.log

Prompty dla Twojego agenta

Te prompty zakładają agenta z podłączonym serwerem MCP dokumentacji OxPHP — sam przeczyta strony, których potrzebuje.

prompt
Napisz plik `.oxphpdeny` dla aplikacji w tym repozytorium. Przeczytaj stronę `security/oxphpdeny` przez serwer MCP dokumentacji OxPHP. Ustal, który katalog jest `DOCUMENT_ROOT` i jakiego trybu routingu używa wdrożenie, a potem znajdź to, co pod tym katalogiem nigdy nie powinno być serwowane — `vendor/`, `composer.*`, pliki konfiguracyjne, zrzuty baz danych, kopie zapasowe, logi, mapy źródeł — oraz które z tych ścieżek aplikacja serwuje sama i zamiast blokady potrzebują reguły `>`. Podaj mi plik z komentarzem przy każdej regule i wypisz wszystko, czego nie może objąć, ponieważ reguły dopasowują ścieżkę żądania, a nie plik, na który się ona rozwiązuje.
prompt
Przenieś reguły blokowania z mojej starej konfiguracji serwera WWW do `.oxphpdeny`. Przeczytaj stronę `security/oxphpdeny` przez serwer MCP dokumentacji OxPHP. Znajdź w tym repozytorium bloki `location` nginx z `deny all` lub `return 403`/`404` oraz reguły `.htaccess` blokujące dostęp i przetłumacz każdą z nich na linię `.oxphpdeny`. Powiedz mi, które z nich obejmuje już blokowanie ścieżek z kropką, a których plik nie potrafi wyrazić.
prompt
Przejrzyj plik `.oxphpdeny` w tym repozytorium pod kątem luk. Przeczytaj stronę `security/oxphpdeny` przez serwer MCP dokumentacji OxPHP. Sprawdź każdą regułę pod kątem tego, jak wdrożona jest ta aplikacja: katalogi zapisane z końcowym ukośnikiem, do których nadal dociera indeks katalogu, nazwy z literami spoza ASCII, które macOS serwuje pod inną pisownią, katalogi zapisywalne, które potrzebują również `PHP_DENY_PATHS`, oraz listę dozwolonych, która zablokowałaby `/.well-known/acme-challenge/`. Podaj mi poprawiony plik i powiedz, co zamyka każda zmiana.

Zobacz też

Znalazłeś błąd? Zgłoś go →