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.
# 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.
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 poziomieDOCUMENT_ROOT. Bez niego wzorzec pasuje na dowolnej głębokości:vendorobejmuje/vendori/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.txtobejmuje/secret.txtna 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.txtnie 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
> /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.
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ęcx[z-a]obejmuje tylkoxz; 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:/[äÄ]dminnie 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.txtjakosecret.txtponiżej co najmniej jednego katalogu (nie samo/secret.txt), czego serwer by nie zrobił, a\/vendornie 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.//vendorivendor//nie pasują do żadnej ścieżki. git kończy wzorzec na bajcie NUL, więcvendor, 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.PHPjako skrypt PHP w każdym systemie plików, a system plików nierozróżniający wielkości liter (macOS, Windows) serwuje/VENDOR/autoload.phpzvendor/, więc reguła dopasowująca jedną pisownię przepuszczałaby pozostałe:*.phpobejmujeshell.PHP, avendor/obejmuje/Vendor/. Reguła!ponownie włącza tylko pisownię, którą wymienia (przy*i!/index.phpścieżka/INDEX.PHPpozostaje 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żeI, a przy/[!i]*.phpserwer uruchomiłby/Info.php. Dopasuj w tym miejscu dowolny znak i ponownie włącz wyjątek, który reguła!dopasuje wtedy dokładnie:/*.phpi!/i*.php. git dopasowuje z rozróżnianiem wielkości liter, chyba że ustawionocore.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.txtserwujelicense.txt:ſ(długie s) jakos,ßiẞjakoss, symbol kelwina (U+212A) jakok, ligaturyfffiflffifflſtstjako 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.txtobejmuje/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
/ädminnie obejmuje/Ädmin, a reguła dlacafé.txtzapisana ze złożonyménie obejmujecafez 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 pozaDOCUMENT_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:
*
!/index.php
!/assets/
!/assets/**
!/.well-known/
!/.well-known/**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 pliksecret.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.jsuruchamiasecret.phpz 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:
/adminz regułąadmin/: w trybie Tradycyjnym router odpowiada na/adminplikiemadmin/index.php, a/adminnie jest żądaniem katalogu./uploads/z regułąuploads/*.php: uruchamia się indeks kataloguuploads/index.php, ponieważ ścieżka żądania to/uploads/./x.php/y.png(PATH_INFO) uruchamiax.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.
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).
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_totalzlicza żądania zablokowane przez regułę. Reguły>to routing, a nie blokady, więc nie są zliczane.oxphp_php_deny_totalnadal zlicza wyłączniePHP_DENY_PATHS; żądanie zablokowane przez oba mechanizmy jest liczone raz, woxphp_path_deny_total. - Na
/configwpisdeny_filepodaje 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. PrzyLOG_LEVEL=debugdla każdej reguły pojawia się liniaoxphpdeny 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 .oxphpdenyna poziomiedebug, ze ścieżką, regułą i numerem jej linii. Żądanie odrzucone z powodu głębokości lub długości logujerequest refused: path too deep or too long to match against .oxphpdenyna poziomiedebug, 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ź404bez 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:
/vendor
/composer.*
/config
*.sql
*.sql.gz
*.bak
*.log/readme.html
/license.txt
/wp-config-sample.php
/wp-content/debug.logW trybie Framework kontroler frontowy otrzymuje /storage/invoices/2026-07.pdf, sprawdza sesję i przesyła plik strumieniowo:
> /storage/invoices/Prompty dla Twojego agenta
Te prompty zakładają agenta z podłączonym serwerem MCP dokumentacji OxPHP — sam przeczyta strony, których potrzebuje.
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.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ć.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ż
- Lista blokowania wykonywania PHP —
PHP_DENY_PATHSiPHP_DENY_FALLBACK - Blokowanie ścieżek z kropką — ukryte pliki i katalogi, odrzucane, zanim zadziałają te reguły
- Routing — tryby routingu i bezpieczeństwo ścieżek
- Metryki —
oxphp_path_deny_total