Разрешённые пути для символьных ссылок

По умолчанию 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.

Конфигурация

bash
# 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

bash
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.

Полученные канонические пути сохраняются как список разрешений. Дубликаты молча устраняются.

Во время запроса слой маршрутизации канонизирует разрешённый путь к файлу и проверяет, что он удовлетворяет одному из условий:

  1. находится внутри DOCUMENT_ROOT, либо
  2. точно совпадает с одной из записей списка разрешений (цели-файлы), либо
  3. начинается с одной из записей списка разрешений, за которой следует / (цели-каталоги).

Та же проверка выполняется второй раз в качестве TOCTOU-защиты внутри пути отдачи статических файлов — после кеша маршрутов, но до любого системного вызова чтения.

Блэклист

Небольшой набор путей никогда не может фигурировать в SYMLINK_ALLOW_PATHS; опечатки и недопонимания иначе резко расширили бы поверхность атаки. Сервер отказывается запускаться, если любая запись разрешается в путь из блэклиста.

Запрещено как точное совпадение:

text
/ /etc /proc /sys /dev /var /home /tmp /root /usr /srv

Запрещено как префикс (запись находится под одним из этих каталогов):

text
/etc /proc /sys /dev /tmp /root /usr
Note

/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 до вытеснения из кеша), поэтому затраты во время выполнения амортизируются

Смотрите также