Разрешённые пути для символьных ссылок
По умолчанию OxPHP отклоняет любой запрос, который разрешается в путь за пределами канонического DOCUMENT_ROOT. Символьная ссылка внутри DOCUMENT_ROOT, указывающая на внешний каталог, возвращает 404, а разрешение пути логирует Blocked request: resolved path escapes document root.
Это правильное поведение по умолчанию: оно останавливает обход каталогов, TOCTOU-атаки с подменой символьных ссылок и случайное раскрытие конфигурационных файлов или секретов, лежащих уровнем выше. Но оно же блокирует легитимные паттерны, которые фреймворки используют уже много лет: php artisan storage:link в Laravel, бандлы ресурсов Symfony, общие тома для загрузок, смонтированные в несколько контейнеров.
SYMLINK_ALLOW_PATHS — это явное согласие на исключение: вы перечисляете пути в файловой системе, в которые разрешено разрешаться символьным ссылкам под DOCUMENT_ROOT. Всё, чего нет в списке, сохраняет строгое поведение с 404.
Конфигурация
# Absolute paths, comma-separated
SYMLINK_ALLOW_PATHS=/var/www/storage,/opt/shared/assets
# Relative paths resolve against DOCUMENT_ROOT
SYMLINK_ALLOW_PATHS=../storage,../shared/uploads
# Mixed
SYMLINK_ALLOW_PATHS=/opt/shared/cdn,../storage/app/publicКогда переменная не задана (по умолчанию), ни одна символьная ссылка не может выйти за пределы DOCUMENT_ROOT.
Пример для Laravel
DOCUMENT_ROOT=/app/public
SYMLINK_ALLOW_PATHS=../storage/app/publicТогда php artisan storage:link создаёт public/storage -> ../storage/app/public внутри проекта. URL-адреса вида /storage/<file> разрешаются через символьную ссылку в /app/storage/app/public/<file>, который канонизируется в путь, авторизованный списком разрешений. Изменений в коде приложения не требуется.
Как это работает
При старте каждая запись разрешается:
- Абсолютные записи — проверяются по блэклисту (см. ниже), затем пропускаются через
realpath(3)(то естьstd::fs::canonicalize). Еслиrealpathзавершается ошибкой (цель не существует), сервер отказывается запускаться. - Относительные записи — соединяются с каноническим
DOCUMENT_ROOT, затем обрабатываютсяrealpath.
Полученные канонические пути сохраняются как список разрешений. Дубликаты молча устраняются.
Во время запроса слой маршрутизации канонизирует разрешённый путь к файлу и проверяет, что он удовлетворяет одному из условий:
- находится внутри
DOCUMENT_ROOT, либо - точно совпадает с одной из записей списка разрешений (цели-файлы), либо
- начинается с одной из записей списка разрешений, за которой следует
/(цели-каталоги).
Та же проверка выполняется второй раз в качестве TOCTOU-защиты внутри пути отдачи статических файлов — после кеша маршрутов, но до любого системного вызова чтения.
Блэклист
Небольшой набор путей никогда не может фигурировать в SYMLINK_ALLOW_PATHS; опечатки и недопонимания иначе резко расширили бы поверхность атаки. Сервер отказывается запускаться, если любая запись разрешается в путь из блэклиста.
Запрещено как точное совпадение:
/ /etc /proc /sys /dev /var /home /tmp /root /usr /srvЗапрещено как префикс (запись находится под одним из этих каталогов):
/etc /proc /sys /dev /tmp /root /usr/var, /home и /srv запрещены только как точное совпадение: голый /srv отклоняется, но /srv/myapp/storage разрешён, точно так же как разрешены /var/www/storage и /home/<any>/.... Записи проверяются дважды: один раз против сырого пути, предоставленного администратором (чтобы путь в стиле macOS /etc -> /private/etc не смог протащить заблэкличенный путь через realpath), и один раз против канонической формы (эшелонированная защита от выхода через цель символьной ссылки).
Сам блэклист зашит в код; переменной окружения для его расширения нет. По умолчанию используется консервативный минимум, отлавливающий ошибки уровня опечаток; администраторы, которым нужны более строгие политики, должны выстраивать их снаружи (права доступа в файловой системе, ограничения монтирования контейнеров, профили AppArmor/SELinux).
Режимы отказа
| Ошибка конфигурации | Результат |
|---|---|
| Цель записи не существует на диске | Сервер отказывается запускаться, ошибка называет запись и canonicalize |
| Запись совпадает с блэклистом (сырая или каноническая) | Сервер отказывается запускаться, ошибка называет запись и правило блэклиста |
| Пустая переменная окружения или только из пробелов | Трактуется как незаданная — строгое поведение по умолчанию |
| Дублирующиеся записи | Молча устраняются после канонизации |
| При старте символьной ссылки ещё нет | Список разрешений регистрируется, но остаётся неактивным, пока не появится символьная ссылка; ни одна стартовая проверка не требует наличия символьной ссылки |
Замечания по безопасности
- Список разрешений работает по принципу явного согласия — безопасное поведение по умолчанию «никаких выходов» сохраняется, пока переменная не задана
- Записи канонизируются при старте, поэтому
..и промежуточные символьные ссылки в пути записи сворачиваются до сохранения - Канонизация во время выполнения закрывает окно TOCTOU при подмене символьной ссылки — проверенный путь и есть тот путь, который будет прочитан
- Цели-файлы совпадают точно; цели-каталоги совпадают по префиксу каталога. Указание файла
/etc/passwdвсё равно было бы отклонено блэклистом, но в более общем случае: указание одного файла/opt/shared/license.keyне даёт неявного доступа к соседним файлам - Результаты проверки путей кешируются для каждого запрошенного URL (один
realpathна уникальный URL до вытеснения из кеша), поэтому затраты во время выполнения амортизируются
Смотрите также
- Запрет путей для PHP — блокировка выполнения PHP по определённым URI-глобам; ортогонально политике символьных ссылок
- Блокировка путей с точкой — отклоняет обход в стиле
.well-knownи утечки dot-файлов - Доверенные прокси — отдельная граница доверия для заголовков
X-Forwarded-* - Справочник по конфигурации — все переменные окружения