Правила заборони шляхів (.oxphpdeny)

Файл .oxphpdeny у корені DOCUMENT_ROOT містить перелік шляхів запитів, які сервер не повинен віддавати напряму. Він записується в синтаксисі .gitignore з одним доповненням: правило з префіксом > не забороняє запит, а передає його вхідному скрипту застосунку.

.oxphpdeny
# 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-скриптів. Вони перевіряються до будь-якого звернення до диска, тож заборонений шлях отримує ту саму відповідь незалежно від того, чи існує файл.

Note

Файл читається один раз, під час запуску: зміни набувають чинності після наступного запуску. Без файлу нічого не змінюється. Сам файл ніколи не віддається. Як і будь-який сегмент шляху, що починається з . (крім початкового /.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.

Правила >: передача запиту застосунку

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

Запит, що збігся з правилом >, не віддається з диска. Він потрапляє до вхідного скрипту з незмінним REQUEST_URI — так само, як запит до відсутнього файлу, — і застосунок може перевірити сесію та сам віддати файл потоком. Вхідний скрипт — це фронт-контролер (ENTRY_FILE=index.php) або воркер. Між > і шаблоном допускаються пробіли й табуляції; \> означає буквальний >. Правило > не можна інвертувати (!>x — помилка), і > без шаблону — теж помилка.

Правило заборони й далі діє всередині каталогу >: за > /storage/invoices/ і *.sql шлях /storage/invoices/dump.sql заборонено, незалежно від того, який рядок іде першим. Правило !, що знімає заборону, залишає файл каталогу >, як у git: додайте !/storage/invoices/dump.sql після *.sql, і цей запит отримає вхідний скрипт. Усередині каталогу >, як і всередині будь-якого виключеного каталогу, ! нічого не включає назад, тож файл і далі не віддається з диска.

Warning

Правилам > потрібен вхідний скрипт. У традиційному режимі та режимі 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, лігатури ff fi fl ffi ffl ſt st — як літери, які вони поєднують, а грецький знак питання (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 — — заборонено заборонено

Голий / ніколи не збігається, тож файл у стилі списку дозволених і далі віддає корінь сайту:

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

Залиште рядки з /.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, як описано вище.
Tip

Щоб закрити каталог, пишіть його без кінцевої скісної риски (/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).

Note

У режимі 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.
  • Перезавантаження немає. Щоб застосувати зміни, перезапустіть сервер.

Приклади

Застарілий застосунок, що віддається з кореня проєкту:

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

Промпти для вашого агента

Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.

prompt
Напиши файл `.oxphpdeny` для застосунку в цьому репозиторії. Прочитай сторінку `security/oxphpdeny` через MCP-сервер документації OxPHP. Зʼясуй, який каталог є `DOCUMENT_ROOT` і який режим маршрутизації використовує розгортання, потім знайди, що в цьому каталозі ніколи не повинно віддаватися, — `vendor/`, `composer.*`, файли конфігурації, дампи баз даних, резервні копії, логи, source maps, — і які з цих шляхів застосунок віддає сам, тож їм натомість потрібне правило `>`. Дай мені файл із коментарем до кожного правила й перелічи все, чого він не може охопити, бо правила зіставляються зі шляхом запиту, а не з файлом, до якого той розвʼязується.
prompt
Перенеси правила заборони з конфігурації мого старого вебсервера в `.oxphpdeny`. Прочитай сторінку `security/oxphpdeny` через MCP-сервер документації OxPHP. Знайди в цьому репозиторії блоки nginx `location`, що виконують `deny all` або `return 403`/`404`, і правила `.htaccess`, що забороняють доступ, та перетвори кожне на рядок `.oxphpdeny`. Скажи, які з них уже покриває блокування шляхів із крапкою, а які файл не може виразити.
prompt
Перевір файл `.oxphpdeny` у цьому репозиторії на прогалини. Прочитай сторінку `security/oxphpdeny` через MCP-сервер документації OxPHP. Звір кожне правило з тим, як розгортається цей застосунок: каталоги, записані з кінцевою скісною рискою, до яких усе ще веде індекс каталогу; імена з літерами поза ASCII, які macOS віддає під іншим написанням; каталоги, доступні для запису, яким також потрібен `PHP_DENY_PATHS`; і список дозволених, який заблокував би `/.well-known/acme-challenge/`. Дай мені виправлений файл і скажи, що закриває кожна зміна.

Див. також