Правила заборони шляхів (.oxphpdeny)
Файл .oxphpdeny у корені DOCUMENT_ROOT містить перелік шляхів запитів, які сервер не повинен віддавати напряму. Він записується в синтаксисі .gitignore з одним доповненням: правило з префіксом > не забороняє запит, а передає його вхідному скрипту застосунку.
# Never served — answered with PHP_DENY_FALLBACK (404 by default)
/vendor
/composer.*
*.sql
*.bak
# Not served directly — the front controller decides
> /storage/invoices/Він призначений для файлів, яким не місце в корені документів, але які там часто опиняються: vendor/ і composer.json у застарілому застосунку, що віддається з кореня проєкту, дампи баз даних і резервні копії, артефакти збирання та source maps у каталозі public/ фреймворку. Правила діють в усіх чотирьох режимах маршрутизації — і для статичних файлів, і для PHP-скриптів. Вони перевіряються до будь-якого звернення до диска, тож заборонений шлях отримує ту саму відповідь незалежно від того, чи існує файл.
Файл читається один раз, під час запуску: зміни набувають чинності після наступного запуску. Без файлу нічого не змінюється. Сам файл ніколи не віддається. Як і будь-який сегмент шляху, що починається з . (крім початкового /.well-known/), він відхиляється ще до маршрутизації блокуванням шляхів із крапкою.
Синтаксис
Усе, що підтримує .gitignore, підтримує й .oxphpdeny:
- Порожні рядки й рядки, що починаються з
#, пропускаються. Мітка порядку байтів UTF-8 (BOM) і закінчення рядків CRLF допускаються. - Пробіли в кінці рядка відкидаються, якщо їх не екрановано (
\). !знову включає те, що виключило попереднє правило.\#і\!на початку рядка означають буквальні#і!.- Кінцевий
/означає, що правило збігається лише з каталогами. /на початку або в середині привʼязує шаблон до кореняDOCUMENT_ROOT. Без нього шаблон збігається на будь-якій глибині:vendorохоплює і/vendor, і/lib/vendor.*і?не виходять за межі одного сегмента шляху;[abc],[a-z],[!0-9]і[^0-9]— класи символів. Як і в git,?і клас збігаються з одним байтом, а клас ніколи не збігається з/:[!-]— це будь-який байт імені, крім-. Літера поза ASCII займає два байти або більше, тож замінити її може лише*: пишіть/caf*.txt, а не/caf?.txt. Усередині класу не підтримуються POSIX-класи, екранування зворотною скісною рискою, символи поза ASCII,-одразу після діапазону та діапазон у зворотному порядку, а інвертований клас у правилі заборони чи правилі>не може містити ASCII-літеру (див. Відмінності від.gitignore).**/на початку,/**у кінці та/**/у середині збігаються через каталоги;**у будь-якому іншому місці — це звичайна*. Довша послідовність*вважається**, як у git:***/secret.txtохоплює/secret.txtна будь-якій глибині.- Перемагає останнє правило, що збіглося, і нічого всередині виключеного каталогу не можна включити назад: якщо виключено
private/, правило!private/ok.txtні на що не впливає. Щоб виключити вміст каталогу, але залишити деякі файли, виключіть/private/*і включіть назад!/private/ok.txt.
Правила >: передача запиту застосунку
> /storage/invoices/
> *.mapЗапит, що збігся з правилом >, не віддається з диска. Він потрапляє до вхідного скрипту з незмінним REQUEST_URI — так само, як запит до відсутнього файлу, — і застосунок може перевірити сесію та сам віддати файл потоком. Вхідний скрипт — це фронт-контролер (ENTRY_FILE=index.php) або воркер. Між > і шаблоном допускаються пробіли й табуляції; \> означає буквальний >. Правило > не можна інвертувати (!>x — помилка), і > без шаблону — теж помилка.
Правило заборони й далі діє всередині каталогу >: за > /storage/invoices/ і *.sql шлях /storage/invoices/dump.sql заборонено, незалежно від того, який рядок іде першим. Правило !, що знімає заборону, залишає файл каталогу >, як у git: додайте !/storage/invoices/dump.sql після *.sql, і цей запит отримає вхідний скрипт. Усередині каталогу >, як і всередині будь-якого виключеного каталогу, ! нічого не включає назад, тож файл і далі не віддається з диска.
Правилам > потрібен вхідний скрипт. У традиційному режимі та режимі SPA вони зупиняють запуск.
Відмінності від .gitignore
- Некоректний рядок зупиняє запуск із зазначенням шляху до файлу й номера рядка; так само зупиняє його файл не в кодуванні UTF-8. git мовчки пропускає такі рядки.
{a,b}розуміється буквально. Фігурні дужки збігаються самі із собою, як у git.- Клас, що містить
/, зупиняє запуск — чи то напряму ([-/]), чи через діапазон, що його перетинає, як-от[+-0]. git приймає такий клас і ніколи не зіставляє ним/; пишіть роздільник поза класом. - POSIX-клас, зворотна скісна риска всередині класу або
-одразу після діапазону зупиняють запуск:[[:digit:]],[\]],[a-b-c]. git тлумачить кожен із них по-своєму. Пишіть натомість діапазон ([0-9]), ставте]першим, щоб зіставити саму дужку, а буквальний-— першим або останнім ([]-],[-a-c]). - Діапазон у зворотному порядку зупиняє запуск, як-от
[z-a]або[a--]. git читає його як лише перший символ, тожx[z-a]охоплює тількиxz; напишіть цей символ або поставте меншу межу першою. - Клас, що містить символ поза ASCII, зупиняє запуск, наприклад
[äÄ]. git зіставляє клас з одним байтом, як і сервер, а такий символ займає два байти або більше:/[äÄ]dminне охопив би ні/ädmin, ні/Ädmin, а/[äÄ]*охопив би/öffnungszeiten, що починається з того самого байта. Поставте на його місце*. - Екранований
/зупиняє запуск (**\/secret.txt,\/vendor). git читає**\/secret.txtякsecret.txtщонайменше на один каталог нижче (але не сам/secret.txt), чого сервер не робив би, а\/vendorне збігається з жодним шляхом. Роздільник ніколи не потребує екранування: пишіть/. - Рядок, що охоплює не те, що в ньому написано, зупиняє запуск — тобто рядок, який починається з пробілу чи табуляції, закінчується неекранованою табуляцією, містить
//будь-де або містить байт NUL. git приймає кожен із них. Пробільні символи на будь-якому кінці стають частиною імені:> /storage/invoices/із відступом — це правило заборони для шляху, що починається з пробілу, а не правило>. Екрануйте такий символ через\, коли імʼя справді з нього починається або ним закінчується.//vendorіvendor//не збігаються з жодним шляхом. git завершує шаблон на NUL, тожvendor, NUL і подальший текст охоплюють у git/vendor, тоді як сервер прочитав би весь рядок і охопив би лише імʼя, що містить цей NUL. Файл, збережений в UTF-16, має NUL після кожного ASCII-символу: зберігайте його в UTF-8. - Рядок лише з пробілів і табуляцій вважається порожнім. git читає рядок, що містить табуляцію, як шаблон для імені, складеного з цих пробільних символів.
- Правила заборони та правила
>ігнорують регістр ASCII-літер; правила!— ні. Сервер виконує/uploads/shell.PHPяк PHP-скрипт на будь-якій файловій системі, а нечутлива до регістру (macOS, Windows) віддає/VENDOR/autoload.phpзvendor/, тож правило, що збігалося б лише з одним написанням, пропускало б інші:*.phpохоплюєshell.PHP, аvendor/—/Vendor/. Правило!включає назад лише те написання, яке воно називає (за*і!/index.phpшлях/INDEX.PHPлишається забороненим), тож ігнорування регістру ніколи не дає серверу віддати з диска шлях, який за точного зіставлення було б від нього закрито. Щоб так і лишалося, інвертований клас у правилі заборони чи правилі>не може містити ASCII-літеру, а клас, що її містить, зупиняє запуск: через ігнорування регістру[!i]не охоплював би йI, і за правила/[!i]*.phpвиконувався б/Info.php. Зіставте в цьому місці будь-який символ і включіть виняток назад — правило!зіставить його точно:/*.phpі!/i*.php. git зіставляє з урахуванням регістру, якщо не заданоcore.ignorecase. - Кілька літер поза ASCII читаються як ASCII. Нечутлива до регістру файлова система (macOS) читає їх саме так і віддає
license.txtна запит/licenſe.txt:ſ(довге s) — якs,ßіẞ— якss, знак кельвіна (U+212A) — якk, лігатуриfffiflffifflſtst— як літери, які вони поєднують, а грецький знак питання (U+037E) і варію (U+1FEF) — як;і`. Правила заборони та правила>теж читають їх так — і в запиті, і в правилі:/license.txtохоплює/licenſe.txt, а/straße/—/strasse/. Правило й далі охоплює те, що охоплювало в записаному вигляді, а правила!читають ці літери лише так, як їх записано, тож заборона може тільки розширитися. - Інші літери поза ASCII збігаються лише в записаному вигляді, як і кожна форма Unicode для імені. Правило для
/ädminне охоплює/Ädmin, а правило дляcafé.txt, записане з попередньо складенимé, не охоплюєcafeіз комбінувальним знаком наголосу (U+0301). Більшість файлових систем Linux зберігає кожне написання як окремий файл, тож це важливо на macOS, яка вважає обидві пари одним файлом, зокрема й через bind mount з macOS у Docker Desktop. Там запит до іншого написання віддається з диска. Ставте*на місце таких літер (/caf*.txt), оскільки?і клас збігаються з одним байтом, або тримайте ці файли позаDOCUMENT_ROOT. - Щонайбільше 256 правил, кожне не довше за 128 байтів. Правило, що виходить за будь-яку з цих меж, зупиняє запуск із зазначенням номера рядка (див. Обмеження).
- Один файл, у корені
DOCUMENT_ROOT, що читається під час запуску. Файли в підкаталогах не читаються. Це має бути звичайний файл розміром не більше 1 MiB; символьне посилання на такий файл теж підходить. FIFO або пристрій зупиняють запуск, не будучи прочитаними, а більший файл зупиняє його, щойно прочитано перший MiB.
Що зіставляється
Правила зіставляються зі шляхом запиту після percent-декодування та нормалізації. Порожні сегменти відкидаються, тож //vendor/x, /vend%6Fr/x і /vendor%2Fx — це все /vendor/x. Сегмент, що починається з . (., .., .git), ніколи не доходить до правил: запит відхиляється звичайним 404 ще раніше, без PHP_DENY_FALLBACK і без підрахунку. Виняток — початковий /.well-known/ (RFC 8615), який блокування шляхів із крапкою пропускає, а правила бачать як будь-який інший шлях.
Запит є запитом до каталогу, коли його декодований шлях закінчується на / (/admin/, /admin%2F, /admin//). Правило з кінцевою скісною рискою збігається лише з такими запитами та з усім, що лежить під ними:
| Правило | /admin |
/admin/ |
/admin/x |
/admin/x/y |
|---|---|---|---|---|
admin/ |
— | заборонено | заборонено | заборонено |
admin або /admin |
заборонено | заборонено | заборонено | заборонено |
admin/*, admin/x |
— | — | заборонено | заборонено |
Голий / ніколи не збігається, тож файл у стилі списку дозволених і далі віддає корінь сайту:
*
!/index.php
!/assets/
!/assets/**
!/.well-known/
!/.well-known/**Залиште рядки з /.well-known/, якщо сайт поновлює TLS-сертифікат через ACME HTTP-01. Без них /.well-known/acme-challenge/<token> буде заборонено.
Перевіряються ще два прочитання запиту, і заборона за будь-яким із них залишається в силі:
- Запит до каталогу також перевіряється як файл, який він називає. Маршрутизатор відкидає кінцеву скісну риску й може віддати
/secret.txt/як файлsecret.txt. - У традиційному режимі сегмент
.php, за яким іде ще шлях, перевіряється як скрипт, який він називає./secret.php/x.jsвиконуєsecret.phpіз PATH_INFO/x.js, тож його заборонено скрізь, де заборонено/secret.php.
В обох випадках !*/ — спосіб gitignore включити назад усі каталоги — не може повернути заборонений файл. У списку дозволених включайте каталог назад без скісної риски (!/docs), щоб сам /docs/ лишався доступним. Жоден інший файл, до якого маршрутизатор розвʼязує запит, не перевіряється, як показує наступний розділ.
Шлях запиту, а не файл
Правила бачать шлях, який запросив клієнт, а не файл, до якого маршрутизатор його розвʼязує. Запит, що розвʼязується до іншого файлу, перевіряється за власним шляхом:
/adminіз правиломadmin/: у традиційному режимі маршрутизатор відповідає на/adminфайломadmin/index.php, а/adminне є запитом до каталогу./uploads/із правиломuploads/*.php: виконується індекс каталогуuploads/index.php, бо шлях запиту —/uploads/./x.php/y.png(PATH_INFO) виконуєx.php, хоча шлях закінчується на.png. Скрипт названо в шляху, тож традиційний режим перевіряє запит також як/x.php, як описано вище.
Щоб закрити каталог, пишіть його без кінцевої скісної риски (/admin). Щоб заборонити виконання .php у каталогах, доступних для запису, використовуйте також PHP_DENY_PATHS: він перевіряє й скрипт, до якого розвʼязується запит.
Режими маршрутизації
| Режим маршрутизації | Правила заборони | Правила > |
|---|---|---|
Традиційний (без ENTRY_FILE) |
Так | Помилка запуску: вхідного скрипту немає |
SPA (ENTRY_FILE=index.html) |
Так | Помилка запуску, оскільки index.html не може прийняти запит |
Фреймворк (ENTRY_FILE=index.php) |
Так | Фронт-контролер |
Worker (WORKER_MODE_ENABLED=true) |
Так, із резервною відповіддю у вигляді коду статусу | Вхідний скрипт воркера |
Резервна відповідь
Правило заборони відповідає згідно з PHP_DENY_FALLBACK — налаштуванням, яке використовує й PHP_DENY_PATHS: HTTP-статусом (за замовчуванням 404; поєднується з ERROR_PAGES_DIR) або PHP-скриптом усередині DOCUMENT_ROOT. Скрипт отримує OXPHP_DENIED_PATH (шлях запиту з початковим /) і OXPHP_DENIED_PATTERN (правило в тому вигляді, як воно записане у файлі, наприклад /composer.*).
Скрипт може лежати за шляхом, який файл забороняє. /_security/ приховує його від прямих запитів, а на прямий запит до нього відповідає сам скрипт — як на заборону. PHP_DENY_FALLBACK читається лише тоді, коли у файлі є принаймні одне правило заборони (або коли діє PHP_DENY_PATHS — у традиційному режимі та режимі SPA).
У режимі Worker резервною відповіддю має бути код статусу: потік воркера не виконує нічого, крім вхідного скрипту воркера.
Коли запит збігається і з .oxphpdeny, і з PHP_DENY_PATHS, відповідає .oxphpdeny.
Спостережуваність
- Метрика
oxphp_path_deny_totalрахує запити, заборонені правилом. Правила>— це маршрутизація, а не заборони, тож вони не враховуються.oxphp_php_deny_totalі далі рахує лишеPHP_DENY_PATHS; запит, який забороняють обидва механізми, враховується один раз — вoxphp_path_deny_total. - На
/configзаписdeny_fileпоказує кількість правил за діями та вид резервної відповіді, але ніколи не самі правила (див. Внутрішній сервер). - Лог запуску повідомляє
loaded 5 rules (3 deny, 1 entry, 1 allow) from /var/www/html/public/.oxphpdeny. ЗаLOG_LEVEL=debugдля кожного правила виводиться рядокoxphpdeny rule, що показує, як його прочитано: номер рядка, дія, шаблон у записаному вигляді та glob, у який його скомпільовано. - Для кожного запиту кожна заборона записує в лог
request denied by .oxphpdenyна рівніdebugзі шляхом, правилом і номером його рядка. Запит, відхилений через глибину чи довжину шляху, записує в логrequest refused: path too deep or too long to match against .oxphpdenyна рівніdebugіз кількістю сегментів і довжиною; він не враховується.
Помилки запуску
| Умова | Результат |
|---|---|
Немає .oxphpdeny |
Функцію вимкнено; нічого не змінюється |
| Файл не читається або не в UTF-8, чи символьне посилання з відсутньою ціллю | Помилка запуску |
| Некоректний рядок | Помилка запуску з <path>: line N: <message> |
| Понад 256 правил або шаблон, довший за 128 байтів | Помилка запуску з номером рядка |
Правило > у традиційному режимі чи режимі SPA |
Помилка запуску |
Режим Worker, правило заборони та скрипт у PHP_DENY_FALLBACK |
Помилка запуску |
Некоректний PHP_DENY_FALLBACK і принаймні одне правило заборони |
Помилка запуску |
Порожній файл, лише коментарі або лише правила ! |
Забороняти нічого: маршрутизація не змінюється, зокрема й обмеження глибини; /config показує файл як завантажений разом із кількістю правил |
oxphp config --check повідомляє про помилки у файлі ще до розгортання (config: INVALID). Перевірки резервної відповіді виконуються, коли сервер будує маршрутизацію, тобто під час запуску.
Обмеження
- Глибину й довжину шляху обмежено. Якщо є принаймні одне правило заборони чи правило
>, на шлях запиту, глибший за 64 сегменти або такий, у якого добуток кількості сегментів на довжину перевищує 128 KiB, повертається404без зіставлення. Зіставлення повторно перевіряє кожен батьківський каталог і сканує шлях кожного з них, тож інакше його вартість зростала б з обома величинами. Шлях до 2 KiB відхиляється лише через глибину; понад це бюджет вичерпується за 16 сегментів при 8 KiB або за 3 сегменти приблизно при 43 KiB. - Файл містить не більше 256 правил, а кожен шаблон — не більше 128 байтів, не враховуючи початкового
!чи>. Шлях запиту зіставляється з правилами по черзі, а також із кожним батьківським каталогом, тож ці обмеження й попереднє обмежують вартість запиту. Типовий файл займає значно менше мілісекунди на запит. Файл на обох межах, навмисно побудований повільним (256 довгих правил, що починаються з/x/**/*, проти шляхів, підібраних так, щоб майже з ними збігатися), займає близько 40 ms на запит і тримає близько 50 MB стану зіставлення на кожен робочий потік Tokio. - Про чутливість до регістру див. Відмінності від
.gitignore. - Перезавантаження немає. Щоб застосувати зміни, перезапустіть сервер.
Приклади
Застарілий застосунок, що віддається з кореня проєкту:
/vendor
/composer.*
/config
*.sql
*.sql.gz
*.bak
*.log/readme.html
/license.txt
/wp-config-sample.php
/wp-content/debug.logУ режимі фреймворку фронт-контролер отримує /storage/invoices/2026-07.pdf, перевіряє сесію та віддає файл потоком:
> /storage/invoices/Промпти для вашого агента
Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.
Напиши файл `.oxphpdeny` для застосунку в цьому репозиторії. Прочитай сторінку `security/oxphpdeny` через MCP-сервер документації OxPHP.
Зʼясуй, який каталог є `DOCUMENT_ROOT` і який режим маршрутизації використовує розгортання, потім знайди, що в цьому каталозі ніколи не повинно віддаватися, — `vendor/`, `composer.*`, файли конфігурації, дампи баз даних, резервні копії, логи, source maps, — і які з цих шляхів застосунок віддає сам, тож їм натомість потрібне правило `>`. Дай мені файл із коментарем до кожного правила й перелічи все, чого він не може охопити, бо правила зіставляються зі шляхом запиту, а не з файлом, до якого той розвʼязується.Перенеси правила заборони з конфігурації мого старого вебсервера в `.oxphpdeny`. Прочитай сторінку `security/oxphpdeny` через MCP-сервер документації OxPHP.
Знайди в цьому репозиторії блоки nginx `location`, що виконують `deny all` або `return 403`/`404`, і правила `.htaccess`, що забороняють доступ, та перетвори кожне на рядок `.oxphpdeny`. Скажи, які з них уже покриває блокування шляхів із крапкою, а які файл не може виразити.Перевір файл `.oxphpdeny` у цьому репозиторії на прогалини. Прочитай сторінку `security/oxphpdeny` через MCP-сервер документації OxPHP.
Звір кожне правило з тим, як розгортається цей застосунок: каталоги, записані з кінцевою скісною рискою, до яких усе ще веде індекс каталогу; імена з літерами поза ASCII, які macOS віддає під іншим написанням; каталоги, доступні для запису, яким також потрібен `PHP_DENY_PATHS`; і список дозволених, який заблокував би `/.well-known/acme-challenge/`. Дай мені виправлений файл і скажи, що закриває кожна зміна.Див. також
- Список заборони виконання PHP —
PHP_DENY_PATHSіPHP_DENY_FALLBACK - Блокування шляхів із крапкою — приховані файли й каталоги, що відхиляються ще до того, як спрацюють ці правила
- Маршрутизація — режими маршрутизації та безпека шляхів
- Метрики —
oxphp_path_deny_total