Обход директорий в эндпоинтах загрузки: как обнаружить и исправить
Обход директорий в endpoint'ах загрузки может раскрыть приватные файлы. Научитесь безопасно обрабатывать имена файлов с allowlist, каноническими путями и контролем доступа на уровне хранилища.

Почему endpoint'ы загрузки легко сделать неправильно
Эндпоинт загрузки должен выполнять одну простую задачу: когда авторизованный пользователь нажимает "Скачать", сервер находит нужный файл и отправляет его обратно (с правильным именем файла и Content‑Type).
На практике шаг «найти файл» — где всё ломается. Самая распространённая ошибка — считать ввод пользователя безопасным именем файла. Разработчик берёт что‑то вроде ?file=invoice.pdf и приклеивает к папке, предполагая, что пользователь будет запрашивать только свои файлы. Злоумышленники не думают в терминах имён файлов — они думают в терминах путей.
Вот в чём главный риск обхода путей в endpoint'ах загрузки: атакующий пытается контролировать, откуда сервер читает данные, а не только какой документ получить. Если код строит путь из ненадёжного ввода, запрос может выйти за пределы намеренной папки и попасть в чувствительные области.
Когда это происходит, последствия могут быть гораздо серьёзнее, чем «кто‑то скачал не тот счёт». Атакующий может прочитать файлы конфигурации с учётными данными БД или ключами API, исходный код (помогает искать новые ошибки), приватные загрузки пользователей и экспорты или другие файлы сервера, которые помогают ему картировать окружение.
Endpoint'ы загрузки просто ошибаются, потому что кажутся безобидными. Их часто добавляют поздно, меньше проверяют, и они «работают нормально» в базовых тестах. Баг проявится только когда кто‑то пришлёт странные входные данные, закодированные пути или неожиданные разделители.
Как обход пути проявляется в реальных запросах
Обычно endpoint загрузки берёт что‑то из запроса и превращает в путь к файлу. Баг проявляется, когда сервер доверяет этому вводу. Если код делает baseDir + "/" + filename, атакующий может изменить смысл «filename».
Снаружи нормальный запрос просит report.pdf. Атакующий пробует полезные нагрузки, которые поднимаются по директориям или прыгают на другой диск.
Типичные формы полезных нагрузок:
../secrets.envили../../../../etc/passwd..\\..\\Windows\\System32\\drivers\\etc\\hosts/etc/passwd(абсолютный путь в Linux)C:\\Windows\\win.ini(абсолютный путь в Windows)- Смешанные разделители, например
..\\../..\\config.yml
Кодирование усложняет обнаружение в логах. Многие приложения декодируют параметры URL перед валидацией, так что заблокированная строка может появиться после декодирования. Атакующие часто пробуют %2e%2e%2f (становится ../) или двойное кодирование %252e%252e%252f (декодируется двойным слоем в некоторых стеках). Они также смешивают регистр и разделители, например %2E%2E%5C, чтобы получить ..\\.
Реалистичный сценарий: ваш endpoint GET /download?file=invoice-123.pdf. Если обработчик использует это значение напрямую, атакующий пробует file=..%2f..%2f.env или file=..\\..\\appsettings.json. Если файл существует и процесс может его прочитать, сервер может вернуть его как «скачивание» без явной ошибки.
Различия платформ достаточно важны, чтобы быть опасными. Linux обычно использует / и чувствителен к регистру. Windows принимает \\ и часто терпит /, поддерживает буквы дисков и специальные имена устройств вроде CON и NUL. Если ваша валидация думает только в стиле одной ОС, она может не заметить обход в другом стиле при деплое или при тестах атакующего.
Быстрые способы заметить небезопасную обработку имён файлов
Большинство обходов путей в endpoint'ах загрузки возникают из одной ошибки: вы позволяете запросу решать, откуда читать файл. Часто это видно быстро, если проследить, откуда маршрут загрузки получает ввод и куда тот ввод затем отправляется.
Признаки риска обычно выглядят так:
- query‑параметр вроде
file,path,nameилиdownloadчитается и передаётся вopen(),readFile(),sendFile()или в join путей. - Сервер доверяет заголовку (например,
X-File,Content-Dispositionили дажеReferer) при выборе файла для отдачи. - «Санитизация» делается строковыми приёмами вроде
replace("../", "")или обрезкой точек вместо жёсткого ограничения базовой папки. - Эндпоинт строит путь из пользовательского ввода и базовой папки, но никогда не проверяет, где он окажется после нормализации.
- Загрузки идут прямо с файловой системы приложения, хотя файлы могли бы храниться иначе (object storage, BLOB в базе, сервис загрузок).
Подтвердить подозрение можно несколькими безопасными тестами в dev‑окружении. Если endpoint принимает имя файла, попробуйте значения, которые никогда не должны работать: ../.env, ../../etc/passwd, ..\\..\\windows\\win.ini или URL‑кодированные варианты вроде %2e%2e%2f. Даже если ответ — ошибка, смотрите логи на предмет подсказок (полные разрешённые пути, стек‑трейсы или сообщения об ошибках с упоминанием реальных директорий).
Один распространённый «выглядит безопасно, но нет» паттерн: код проверяет, что ввод заканчивается на .pdf, а затем открывает файл. Атакующие могут использовать кодировки, двойные расширения или другие крайние случаи, чтобы обойти наивные проверки. Даже если старые примеры с нулевым байтом больше не применимы к вашему стеку, урок тот же: проверка суффикса не является надёжной границей.
Более безопасный подход: ид‑основанный доступ и поиск на сервере
Проще всего избежать обхода путей — перестать принимать имена файлов из URL. Имена файлов труднообработать: в них могут быть слеши, обратные слеши, последовательности .. и кодировки, которые меняют смысл после объединения строк в путь.
Более безопасный подход — выдавать стабильный ID файла, например GET /download/7f3a2c, и хранить реальное местоположение на стороне сервера. ID — это просто указатель. Ваше приложение решает, какому файлу он соответствует.
Когда приходит запрос, найдите файл в таблице базы данных (или другом надёжном хранилище) по этому ID. Храните метаданные, которые нужны для безопасного решения: кто владелец, какой ключ в хранилище, какой Content‑Type отдавать и действителен ли файл.
Простой поток:
- Принимайте только непрозрачный ID (UUID, случайный токен, ID из БД).
- Получайте метаданные файла по ID (владелец, организация, ключ в хранилище, Content‑Type).
- Проверяйте, что текущий пользователь имеет право на доступ к записи.
- Скачивайте по доверенному ключу хранилища, а не по вводу пользователя.
- Устанавливайте заголовки ответа из сохранённых метаданных, а не из запроса.
Пример: клиент нажимает "Скачать счёт", UI вызывает /download/inv_12345. Сервер проверяет, что inv_12345 принадлежит аккаунту этого клиента, затем читает storage_key=accounts/889/invoices/2025-01.pdf. Пользователь никогда не сможет отправить ../../etc/passwd, потому что параметра имени файла нет.
Это также упрощает аудит. Вы проверяете одну авторизацию вокруг lookup'а вместо попыток доказать, что каждое возможное имя файла безопасно.
Списки разрешённых значений, которые действительно помогают (и чего избегать)
Allowlist полезен только если он ограничивает загрузки теми объектами, которые вы заранее считаете безопасными. Для загрузок это обычно значит либо (1) список известных ключей в хранилище, которые вы сгенерировали и сохранили, либо (2) короткий список типов файлов, которые вы действительно поддерживаете.
Самая безопасная allowlist — по точному сохранённому ключу, а не по имени файла, введённому пользователем. Пользователь отправляет ID вроде fileId=8f3c..., сервер ищет запись (владелец, путь в бакете, точный ключ объекта) и отдаёт её. Пользователь не влияет на путь.
Если вы вынуждены принимать имена файлов, считайте списки расширений запасной мерой, а не основным контролем. Валидируйте короткий набор (например: pdf, csv) и отклоняйте всё остальное. Не пытайтесь «очистить» ввод и превратить его во что‑то допустимое — отклонение безопаснее.
Перед валидацией нормализуйте ввод, чтобы атакующие не проскочили проверку. Делайте всё просто и предсказуемо: обрежьте пробелы, нормализуйте регистр, если это уместно, и отклоняйте символы разделителей пути (/ и \\) и последовательности вроде ... Также следите за трюковыми именами вроде invoice.pdf.exe или report.pdf .
Чего избегать: блэклисты (они пропускают варианты), проверки endsWith на сыром вводе и allowlist, которая всё ещё допускает части директорий.
Канонические пути: принудительное ограничение базовой директории
Самое безопасное правило: пользователь никогда не должен выбирать файловую систему напрямую. Если вы всё же принимаете что‑то вроде имени файла, преобразуйте это в канонический путь и докажите, что он по‑прежнему находится в одной утверждённой папке (например, downloads_root). Это закрывает классический трюк с ../.
Канонизация значит получение реального пути, который использует ОС. Она должна сворачивать . и .., нормализовать разделители и (если платформа поддерживает) разрешать симлинки. Симлинки важны: без них путь внутри папки загрузок может указывать наружу.
Практический шаблон выглядит так:
base = realpath(downloads_root)
requested = realpath(join(downloads_root, user_input))
if requested is null -> error
if not requested starts_with base + separator -> error
serve requested
Перед канонизацией отклоняйте явно опасный ввод. Это уменьшает число крайних случаев и упрощает логи. Частые отклонения: абсолютные пути (/etc/passwd, C:\\Windows\\...) и ввод, содержащий разделители пути, когда вы ожидаете простое имя файла.
Правила, которые хорошо себя ведут:
- Принимайте только относительные входные данные (без ведущего
/, без букв дисков). - Строьте путь с помощью безопасных join‑функций, а не конкатенцией строк.
- Канонизируйте до реального пути, затем проверьте, что он остаётся в
downloads_root. - Если канонизация не проходит — блокируйте, не пытайтесь «поставить другой».
Наконец, возвращайте одинаковую общую ошибку для "не найдено" и "заблокировано". Различающиеся сообщения позволят атакующим выяснить, какие файлы существуют на сервере, даже если они не могут скачать их.
Контроль доступа на уровне хранилища, который уменьшает радиус поражения
Даже при хорошей валидации относитесь к endpoint'ам загрузки как к высокому риску. Если обход проскочит, выбор хранилища решит — получит ли атакующий один файл или весь сервер.
Избегайте отдачи пользовательских файлов с того же диска, где лежит контейнер приложения. Когда загрузки и код приложения делят файловую систему, одна уязвимость может выдать ключи, конфиги и исходники. Храните файлы вне web root и отключите прямую раздачу статических файлов из этой директории, пусть только приложение читает и возвращает файлы.
Для многих команд object storage — самый простой способ снизить риск. Вместо чтения локальных путей храните файлы как объекты и применяйте доступ на уровне объектов или короткоживущие подписанные ссылки. Приложение проверяет, кто пользователь, какой ID файла запрошен и принадлежит ли он, и только затем создаёт временный токен загрузки или проксирует файл.
Контроли, которые быстро окупаются:
- Держите бакеты приватными по умолчанию.
- Предпочитайте короткоживущие подписанные ссылки вместо постоянных публичных URL.
- Если используете локальный диск — храните файлы вне web root.
- Запускайте приложение с минимальными правами файловой системы (только чтение где нужно).
- Разделяйте окружения и бакеты (dev vs prod).
Логи важны так же, как и блокировки. Для каждого запроса на загрузку логируйте ID пользователя, ID файла и решение (разрешено или заблокировано) плюс код причины, например "не владелец" или "просроченный токен". Этот аудиторский след помогает заметить сканирование и доказывает, что происходило.
Частые ошибки и обходы, которые используют атакующие
Большинство фиксов обхода пути проваливаются, потому что код предполагает, что атакующий пошлёт простой ../. Реальные атаки многослойны и созданы так, чтобы проскользнуть мимо ваших изменений.
Классическая ошибка — доверять правилам на стороне клиента или скрытым полям формы. Если UI позволяет выбирать из выпадающего списка, но сервер всё ещё принимает параметр path, атакующий может изменить это значение в запросе. Сервер должен работать так, как будто UI не существует.
Трюки с кодированием — ещё один частый обход. Команды декодируют один раз, валидируют, а позже какой‑то фреймворк или прокси декодирует снова. Это может превратить безопасную строку в ../ после проверки. Лечение — консистентность: нормализуйте и валидируйте в одном месте и используйте именно это значение для открытия файла.
Проверки расширений легко обойти. Блокировка всего, кроме .pdf, кажется безопасной, но атакующий всё ещё может перейти в чувствительную папку и скачать файл, который случайно заканчивается на .pdf, или воспользоваться лишними сегментами, если обработка пути слабая. Если цель — отдать только счета, путь не должен контролироваться пользователем.
Частые обходы:
- Скрытые или генерируемые клиентом пути, которые принимаются как доверенные
- Декодирование на разных уровнях (приложение, фреймворк, прокси)
- Проверки «разрешить только .pdf», игнорирующие директории и сегменты
- Симлинки в допустимой папке, указывающие наружу
- Zip Slip: распаковка архива с путями
../, при которой файлы записываются вне целевой папки
Симлинки требуют особого внимания. Даже если вы соединяете базовую директорию с именем файла, симлинк внутри этой базовой директории может увести за границу. Надёжный фикс — канонические проверки пути (после разрешения симлинков) плюс строгие права на файловую систему.
Быч чек‑лист перед выпуском фичи загрузки
Endpoint'ы загрузки кажутся простыми, но это частый путь попадания обхода в продакшен. Небольшой выбор (например, принимать имя файла из URL) может превратить обычную загрузку счёта в «прочитать любой файл на сервере».
Самый безопасный подход по умолчанию
Начните с решения, какие файлы пользователь может запрашивать. Пусть пользователь просит файл по ID, который контролирует сервер, а не путь, который он может сконструировать.
- Ввод: принимайте только ID файла (или номер счёта), никогда сырой путь или имя файла.
- Lookup: сопоставляйте этот ID с записью, в которой хранится реальный ключ хранилища или абсолютный путь на сервере.
- Валидация: если приходится обрабатывать имена, используйте жёсткую allowlist (ожидаемые расширения, допустимые символы), затем канонизируйте и подтвердите, что результат остаётся в базовой директории.
- Авторизация: проверяйте владение и роль до чтения файла, а не после.
- Ответ: устанавливайте безопасные заголовки и не отражайте ввод пользователя в
Content-Dispositionбез очистки.
Снизьте радиус поражения через хранилище и тесты
Хранилище по умолчанию приватное спасает вас, когда код ошибается. Держите файлы вне web root и избегайте публичных бакетов, где достаточно угадать имя.
Быстрый практический тест: попробуйте запросы с ../, URL‑кодированными вариантами и лишними слешами против вашего route загрузки. Блокировки должны логироваться с достаточной информацией для отладки (ID пользователя, запрошенный ID, причина блокировки), но без секретов.
Пример сценария: скачивание счёта, которое даёт доступ к файлам сервера
Основатель сделал быстрый функционал счётов: клиенты нажимают кнопку, и приложение вызывает эндпоинт вроде /download?filename=invoice-1042.pdf. Сервер берёт filename, строит путь, читает файл и возвращает его.
В тестах это работает, потому что все запрашивают нормальные файлы. Проблема в том, что сервер доверяет вводу пользователя при выборе файла. Атакующий меняет параметр на ../../.env или ../../../etc/passwd. Если код соединяет строки (или декодирует URL‑значения и затем объединяет), приложение может прочитать файлы вне папки счетов.
План исправления, который сохраняет фичу, но убирает риск:
- Перейти с имён файлов на ID (пример:
/download?id=inv_1042) и смотреть реальный путь на сервере по этому ID. - Применять проверки авторизации и владения, чтобы только верный клиент мог скачать свой счёт.
- Хранить счёта в приватном хранилище (не в публичной папке) и отдавать через контролируемые скачивания.
- Добавить простые allowlist'ы: разрешать только ожидаемые форматы счётов (например, PDF) и отклонять всё остальное.
- Логировать отказанные запросы, чтобы обнаруживать попытки сканирования.
Чтобы подтвердить исправление, не полагайтесь на "кажется норм":
- Попробуйте payload'ы с
../и их URL‑варианты и убедитесь, что всегда возвращается общий отказ. - Проверьте логи, что попытки обхода блокируются и фиксируются.
- Добавьте автоматизированный тест, который пробует
../../.envи ожидает отказ.
Следующие шаги: аудитировать эндпоинты и быстро исправлять
Допустим, у вас больше одного маршрута отдачи файлов. Во многих приложениях файлы отдаются из разных мест: маршрут счётов, экспортов, превью вложений и иногда отладочный хелпер, который случайно попал в прод.
Практический аудит:
- Перечислите все endpoint'ы, которые возвращают файлы (download, export, report, image, attachment, backup).
- Проследите, откуда каждый получает имя файла или путь (query, route param, JSON‑тело, заголовки, БД).
- Запишите все используемые хранилища (локальные директории, временные папки, сетевые тома, бакеты объектного хранилища).
- Поиск по построению путей и чтению файлов (
join,resolve,open,readFile,sendFile, создание zip). - Проверьте проверки доступа: кто может запросить какой файл и как это сопоставление обеспечивается.
Как только вы знаете площадь атаки, проведите фокусную фиксацию. Предпочитайте ID файлов с серверным lookup'ом (ID -> сохранённый путь), вместо того чтобы ввод пользователя влиял на путь в файловой системе. Добавьте канонические проверки пути, чтобы обеспечить единую границу базовой директории для оставшихся обращений к диску, и используйте жёсткую allowlist только для действительно статических файлов.
Завершите тестами, которые доказывают, что фикс остаётся фиксом:
- Запросы с
../и URL‑варианты отклоняются. - Абсолютные пути (Unix и Windows) отклоняются.
- Краевые случаи с симлинками не позволяют выйти за базовую директорию.
- Скачиваются только файлы, принадлежащие текущему пользователю или организации.
Если вы унаследовали код, сгенерированный ИИ, стоит привлечь второе мнение на маршруты отдачи файлов. FixMyMess (fixmymess.ai) специализируется на диагностике и ремонте ИИ‑сгенерированных приложений: быстрый аудит может выявить небезопасные обработчики загрузок, сломанные проверки авторизации, утечки секретов и рискованные паттерны хранения до попадания в прод.
Часто задаваемые вопросы
Что такое переход по пути (path traversal) в эндпоинте загрузки?
Эндпоинт загрузки становится рискованным, когда ввод пользователя превращается в путь к файлу на диске. Если код делает что‑то вроде «базовая папка + ввод пользователя», атакующий может попытаться вставить ../ или закодированные варианты, чтобы выйти за пределы ожидаемой директории и прочитать другие файлы, доступные процессу.
Что на самом деле могут получить злоумышленники, если обход пути работает?
Если атакующий может читать файлы вне папки загрузок, он может получить файлы конфигурации с секретами, исходный код приложения, приватные загрузки пользователей, экспорты или другие системные файлы. Часто реальный ущерб — это последующие шаги, например использование похищенных учётных данных для доступа к базе данных или внешним сервисам.
Как быстро найти небезопасную обработку имён файлов в коде?
Ищите маршруты, которые читают file, path, name или похожие параметры (из query, route param, JSON‑тела или заголовков) и передают их в файловые API вроде open, readFile или sendFile. Также считайте строковые «санитизации» типа replace("../", "") красным флагом — они обычно пропускают кодировки и крайние случаи.
Почему простая блокировка строки «../» не решает проблему?
Потому что атакующие редко шлют простой ../. Они используют URL‑кодирование, двойное кодирование, смешанные разделители и платформенно‑специфичные пути, так что опасная последовательность может появиться только после декодирования или нормализации в стекe. Надёжная стратегия — нормализовать и валидировать в одном месте и быть уверенным, что именно проверённое значение используется для чтения файла.
Какой дизайн эндпоинта загрузки самый безопасный?
Лучше всего — принимать непрозрачный идентификатор файла в URL и делать серверный lookup реального ключа хранения или пути. Пользователь запрашивает «файл 123», а ваша логика решает, где файл хранится и имеет ли текущий пользователь на него права.
Достаточно ли списка разрешённых расширений, например «только .pdf»?
Не надёжно. Проверка вроде «заканчивается на .pdf» не предотвращает обход директорий и не мешает загрузить чувствительный PDF, который случайно существует где‑то ещё на сервере. Используйте проверки расширений как небольшую дополнительную меру после того, как вы устранили возможность управления путём со стороны пользователя.
Что значит «проверка канонического пути» на практике?
Разрешите запрашиваемый путь в каноническом виде и проверьте, что он всё ещё лежит внутри одной утверждённой базовой директории. Если канонизированный результат выходит за границы, блокируйте его и возвращайте общий ответ об ошибке — так атакующий не сможет узнавать, какие файлы существуют.
Почему различия Windows и Linux важны для валидации?
Платформы по‑разному интерпретируют пути: Windows поддерживает обратные слеши, буквы дисков и специальные имена устройств; Linux использует прямые слеши и чувствителен к регистру. Валидация, рассчитанная только на один стиль, может пропускать обходы в другом, поэтому учитывайте особенности целевой среды.
Как выбор хранилища уменьшает ущерб, если баг проскочил?
Не храните файлы, доступные для скачивания, в той же области диска, где лежит код приложения и секреты. Предпочтительнее приватное объектное хранилище с выдачей краткоживущих токенов на скачивание или проксирование через сервер. Запустите приложение с минимальными правами доступа к файловой системе, чтобы один баг не открыл весь сервер.
Что логировать и мониторить для endpoint'ов загрузки?
Логируйте идентификатор пользователя, ID файла (или запрошенное значение) и решение (разрешено/заблокировано) с простой причиной, но не записывайте полные разрешённые пути или содержимое чувствительных файлов. Если у вас код, сгенерированный ИИ, и вы подозреваете проблемные маршруты отдачи файлов, FixMyMess (fixmymess.ai) может быстро провести аудит и исправить небезопасные обработчики, ошибки авторизации и утечки секретов.