Безопасные загрузки файлов: права доступа, лимиты, подписанные URL и сканирование
Безопасные загрузки файлов: жёсткие права, лимиты размеров, подписанные URL и простая схема сканирования, чтобы избежать инцидентов.

Почему загрузки файлов рискованны (простыми словами)
Загрузки файлов кажутся безобидными: фото профиля, PDF, таблица. Но они часто становятся первой причиной инцидентов, потому что позволяют посторонним прислать вашей системе «тайную коробку». Если вы приняли её, сохранили и показываете другим, вы создали новый вектор атаки на приложение.
Риск — это не только «кто‑то загрузил вирус». Неправильная загрузка может слить приватные файлы, раздуть счёт за хранение или заставить пользователей отдать доступ. Файл с именем “invoice.pdf” может вовсе не быть PDF. Даже настоящие PDF и изображения могут создать проблемы, если приложение доверяет метаданным, автоматически генерирует превью или отдает их с неправильными правилами.
Типичные провалы выглядят так:
- Кто‑то угадывает URL файла и скачивает документ другого пользователя.
- Загруженный HTML-файл подаётся как веб‑страница и показывает форму для кражи логина.
- Атакующий многократно загружает огромные файлы, пока приложение не замедлится или не упадёт.
- «Безопасный» тип файла подделывают, после чего сотрудник открывает его во внутренней сети.
Одна деталь объясняет многие инциденты: хранение файлов — это не то же самое, что их выдача. Хранение — где вы держите байты. Выдача — как вы доставляете эти байты в браузеры и приложения. Проблемы возникают, когда приложение отдает пользовательские загрузки с тем же уровнем доверия и правилами, что и основной сайт, и браузер считает загрузку «доверенной».
«Достаточно безопасно» для малого или растущего приложения обычно означает, что вы можете честно ответить на четыре вопроса без уклончивости: кто может загружать, что вы принимаете, какой размер и как часто, и кто сможет читать файл позже. Даже если вы быстро собираете продукт (сгенерированный код или платформа на основе чата), эти ограждения важны.
Простая модель угроз для загрузок
Относитесь к каждой загрузке как к недоверенному входу. Практический способ защитить загрузки — представить, кто может злоупотребить ими и что для него будет «успехом».
Большинство злоумышленников — это либо боты, сканирующие слабые формы загрузки, либо реальные пользователи, пытающиеся получить бесплатное хранилище, собрать данные или потроллить сервис. Иногда это конкурент, ищущий утечки или сбои.
Чего они хотят? Обычно одно из следующих:
- Запустить код на ваших серверах, загрузив что‑то, что будет выполнено.
- Украсть приватные файлы, угадав, повторно использовав или поделившись URL для скачивания.
- Повредить доступности, затопив систему загрузками или вынудив дорогостоящую обработку.
- Увеличить ваш счёт через рост хранения или тяжёлые по трафику скачивания.
Затем сопоставьте слабые точки. Эндпоинт загрузки — это входная дверь (слишком большие файлы, странные форматы, высокая частота запросов). Хранилище — это задняя комната (публичные бакеты, неверные права, общие папки). URL для загрузки — это выход (предсказуемые, долгоживущие или не привязанные к пользователю).
Пример: функция «загрузить резюме». Бот загружает тысячи больших PDF, чтобы накрутить расходы, а злоумышленник заливает HTML-файл и выдаёт его за «документ», чтобы обмануть других.
Прежде чем вводить контролы, решите, что важно для вашего приложения: приватность (кто может читать), доступность (можете ли вы продолжать отдавать), стоимость (хранение и трафик) и соответствие требованиям (где хранится данные и как долго). Этот список приоритетов поможет принимать согласованные решения.
Права доступа и контроль, которые реально работают
Большинство инцидентов с загрузками — это не сложные взломы. Это простые баги «я вижу чужой файл». Рассматривайте права как часть механики загрузок, а не как дополнительную фичу.
Начните с одного правила: по умолчанию отказ. Считайте каждый загруженный объект приватным, пока вы явно не разрешили доступ. «Приватно по умолчанию» — хорошая база для счетов, медицинских документов, документов аккаунта и всего, что привязано к пользователю. Делайте файлы публичными только когда пользователь этого явно ожидает (например, публичный аватар), и даже тогда подумайте о временном доступе.
Роли, соответствующие реальным задачам
Держите роли простыми и разделёнными. Частое разделение:
- Uploader: может создавать загрузки для своей учётной записи
- Viewer: может скачивать файлы, которые ему разрешены
- Support: имеет доступ к файлам только по временной и аудитируемой выдаче прав
- Admin: управляет политиками, но не должен автоматически читать всё
Не полагайтесь на правила уровня папки вроде «всё в /user-uploads/ ок». Проверяйте владение или доступ тенанта при чтении каждого файла. Это защитит, когда кто‑то меняет команду, уходит из организации или файл переназначают.
Хорошая модель для поддержки — узкая и временная: дать доступ к одному конкретному файлу, залогировать и автоматически истечь.
Валидировать тип файла, не доверяя клиенту
Большинство атак на загрузки начинаются с простой уловки: файл выглядит безопасно по имени или заголовку браузера, но на деле — не тот формат. Относитесь как к недоверенным ко всему, что присылает клиент.
Начните с allowlist: решите, какие точные форматы вы принимаете (например, .jpg, .png, .pdf) и отвергайте всё остальное. Избегайте «любое изображение» или «любой документ», если вам действительно не нужно такое широкое принятие.
Не доверяйте расширению имени файла или заголовку Content-Type от клиента. Их легко подделать. Файл с именем invoice.pdf может быть исполняемым, а Content-Type: image/png — ложью.
Надёжнее инспектировать первые байты файла, часто называемые «magic bytes» или сигнатурой файла. Многие форматы имеют стабильные заголовки (PNG, JPEG). Если заголовок не соответствует разрешённому формату — отклоняйте.
Практичная схема валидации:
- Allowlist расширений на сервере
- Определяйте MIME‑тип на сервере (не доверяйтесь клиентскому заголовку)
- Проводите sniffing по magic bytes для поддерживаемых форматов
- Генерируйте случайное имя для хранения и сохраняйте оригинальное имя как метаданные
- Блокируйте рискованные форматы, особенно HTML, SVG и скриптоподобное содержимое
Переименование важнее, чем кажется. Если вы храните имена, предоставленные пользователем, вы открываете путь к трюкам с путями, странными символами и перезаписями. Используйте сгенерированный ID для хранения и храните оригинальное имя только для отображения.
Для фото профиля принимайте только JPEG и PNG, проверяйте заголовки и очищайте метаданные если возможно. Для документов ограничьте PDF и отвергайте всё с активным содержимым. Если позже решите поддержать SVG или HTML — относитесь к ним как к потенциально выполняемым и изолируйте.
Ограничения по размеру, лимиты частоты и основы DoS
Большинство простоев при загрузках — это не «умные» хакерские приёмы. Это гигантские файлы, слишком много запросов или медленные соединения, которые загружают серверы до отказа. Считайте каждый байт затратой.
Устанавливайте лимиты по фиче
Выбирайте максимальный размер для каждой функции, а не одно глобальное число. Аватар не требует того же лимита, что налоговый документ или короткое видео. Ставьте минимально разумный лимит, затем добавляйте отдельный путь для больших файлов только когда он действительно нужен.
Применяйте ограничения в нескольких местах, потому что клиент может врать: в логике приложения, на веб‑сервере/реверс‑прокси, с таймаутами загрузки и с ранним отклонением, когда заявленный размер слишком велик (до чтения всего тела).
Конкретный пример: аватары — до 2 MB, PDF — до 20 MB, всё большее — через другой поток (например, прямой upload в объектное хранилище с подписанным URL).
Лимиты частоты и контроль злоупотреблений
Даже маленькие файлы могут привести к DoS, если их загружают бесконечно. Добавьте rate limit для эндпоинтов загрузки по пользователю и по IP. Для анонимных запросов сделайте ограничения строже.
Возобновляемые (resumable) загрузки помогают реальным пользователям с плохой связью, но session‑токен должен быть жёстким: короткий срок жизни, привязан к пользователю и к конкретному размеру/назначению файла. Иначе endpoint «resume» станет бесплатной трубой в ваше хранилище.
Когда вы блокируете загрузку, возвращайте понятные пользователю ошибки (файл слишком большой, слишком много запросов), но не раскрывайте внутренности (стектрейсы, имена бакетов, детали поставщика).
Безопасные решения для хранения и выдачи файлов
Безопасные загрузки — это не только то, что вы принимаете. Это также где файл хранится и как вы его отдаёте.
Не храните байты файлов в основной базе данных. Большинству приложений нужна только метаинформация в БД (ID владельца, оригинальное имя, определённый тип, размер, контрольная сумма, ключ хранилища, время создания). Байты держите в объектном хранилище или сервисе, предназначенном для больших BLOB.
Разделяйте публичные и приватные файлы на уровне хранения. Используйте разные бакеты/контейнеры с разными правилами. Публичные файлы (публичные аватары) могут читаться без логина. Приватные файлы (контракты, счета, медицинские документы) никогда не должны быть общедоступными, даже если кто‑то угадает URL.
По возможности не отдавайте пользовательские файлы с того же домена, что и основное приложение. Если рискованный файл просочится (HTML, SVG со скриптами или странности MIME sniffing), хостинг на основном домене может привести к захвату аккаунтов. Отдельный домен загрузок или домен хранилища ограничит радиус поражения.
При скачивании заставляйте безопасные заголовки. Ставьте предсказуемый Content-Type на основе разрешённых типов, а не того, что утверждает пользователь. Для всего, что браузер может интерпретировать, предпочитайте отдавать как скачивание.
Несколько дефолтов, предотвращающих сюрпризы:
- Используйте
Content-Disposition: attachmentдля документов. - Устанавливайте безопасный
Content-Type(илиapplication/octet-stream). - Храните и отдавайте с непрозрачными ключами объектов (не пользовательские имена).
- Логируйте скачивания приватных файлов.
Хранение — это также безопасность. Удаляйте брошенные загрузки, удаляйте старые версии после замены и ставьте срок жизни для временных файлов. Менее данных — меньше риска утечки.
Подписанные URL: когда использовать и как ужесточить
Подписанные URL (pre-signed) — распространённый способ позволить пользователям загружать или скачивать файлы, не делая бакет публичным и не пропуская каждый байт через ваш API. В URL заложено временное разрешение, затем оно истекает.
Два распространённых потока:
- Прямая загрузка в хранилище: ваше приложение выдаёт короткоживущий подписанный URL, и браузер загружает прямо в объектное хранилище.
- Загрузка через сервер: файл попадает сначала на ваш API, затем сервер сохраняет его.
Прямая загрузка снижает нагрузку на API, но делает правила хранилища и ограничения в URL еще более критичными.
Как сделать подписанные URL жёсткими
Относитесь к подписанному URL как к одноразовому ключу. Делайте его специфичным и с коротким сроком жизни.
- Истекайте write‑URL быстро (обычно 1–5 минут). Читайте URL лучше держать минуты, а не дни.
- Привязывайте URL к точному ключу объекта (один объект, не папка).
- Добавляйте ограничения, где это поддерживается: ожидаемый content type, максимальный размер, контрольная сумма.
- Выдавайте URL только после проверки прав.
- Логируйте, кто запросил URL и зачем (user ID, ключ объекта, цель, IP/UA).
Практичный паттерн: сначала создайте запись загрузки (status: pending), затем выдайте подписанный URL. После загрузки проверьте, что объект существует и соответствует ожидаемому размеру и типу, прежде чем пометить его как готовый.
Пошагово: безопасный поток загрузки, который можно реализовать
Безопасный поток — это в основном ясные правила и понятные состояния. Относитесь к каждой загрузке как к недоверенной, пока проверки не пройдут.
Опишите, что каждая фича позволяет. Фото профиля и налоговый документ не должны использовать одинаковые типы файлов, лимиты размера или видимость.
Практичный поток (с реальными статусами)
-
Определите допустимые типы и лимит размера по фиче (например: фото до 5 MB; PDF до 20 MB). Применяйте те же правила на бэкенде.
-
Создайте «запись загрузки» до прихода байтов. Храните: владелец (пользователь или организация), назначение (avatar, invoice, attachment), оригинальное имя файла, ожидаемый макс. размер и статус вроде
pending. -
Загружайте в приватное местоположение. Не позволяйте клиенту выбирать конечный путь.
-
Снова проверьте на сервере: размер, magic bytes/тип, allowlist. Если прошло — переведите статус в
uploaded. -
Просканируйте на вредоносное ПО и обновите статус на
cleanилиquarantined. Если сканирование асинхронное, держите доступ закрытым пока ждёте. -
Разрешайте скачивание, превью или обработку только при статусе
clean.
Небольшой пример: для фото профиля создайте запись, привязанную к пользователю с целью avatar, храните приватно, подтвердите, что это реально JPEG/PNG (а не просто переименованный файл), просканируйте, затем сгенерируйте превью URL.
Базовые шаблоны сканирования на вредоносное ПО (без обещаний)
Сканирование — это страховочная сетка, а не гарантия. Оно ловит известные плохие файлы и очевидные трюки, но не всё. Цель простая: снизить риск и по умолчанию сделать неизвестные файлы безвредными.
Надёжный паттерн — сначала карантин. Сохраняйте каждую новую загрузку в приватном месте и помечайте как pending. Только после прохождения проверок перемещайте в «clean» (или помечайте как доступный).
Синхронные сканирования работают только для маленьких файлов и низкого трафика, потому что пользователь ждёт. Большинство приложений сканируют асинхронно: принимают загрузку, возвращают состояние «обработка», сканируют в фоне.
Что обычно включает «базовое сканирование»
Базовое сканирование обычно — это антивирусный движок (или сервис) плюс несколько правил: AV‑скан, проверки типа файла (magic bytes), лимиты для архивов (zip bomb, вложенные zip, огромный распакованный размер) и блокировка форматов, которые вам не нужны.
Если сканер упал, таймаутнулся или вернул «неизвестно», считайте файл подозрительным. Держите его в карантине и не давайте ссылку для скачивания. Именно здесь команды часто обжигаются: «скан не прошёл» не должно превращаться в «всё равно публикуем».
При блокировке файла формулируйте нейтральное сообщение: «Мы не смогли принять этот файл. Попробуйте другой файл или свяжитесь с поддержкой.» Не утверждайте, что нашли вредоносное ПО, если вы не уверены.
FAQ
Что минимально нужно сделать, чтобы загрузки файлов были «достаточно безопасными»?
Начните с принципа приватно по умолчанию и относитесь к каждой загрузке как к недоверенному входному значению. Выполните четыре базовых проверки на сервере:
- Кто может загружать
- Какие типы файлов вы принимаете (allowlist)
- Насколько большой и как часто (лимиты размера и частоты)
- Кто может читать файл позже (проверки прав для каждого файла)
Если вы чётко ответите на эти вопросы, вы уже опережаете большинство инцидентов.
Почему загрузки файлов часто становятся первой точкой взлома?
Потому что пользователь может загрузить «тайную коробку», которую ваше приложение сохранит и затем может показать другим. Это ведёт к таким проблемам:
- Несанкционированный доступ к приватным документам
- Фишинг или захват аккаунта, если файл подаётся как доверимое веб-содержимое
- Сбои и большие счета из‑за массовых загрузок или гигантских файлов
Редко дело сводится только к «кто‑то загрузил вирус».
В чем разница между хранением файлов и их выдачей, и почему это важно?
Хранение — это место, где вы держите байты. Отдача (serving) — это как вы передаёте эти байты браузерам и приложениям.
Опасность возникает, когда приложение отдаёт загруженные пользователями файлы с тем же уровнем доверия и правилами, что и основной сайт. В таком случае браузер может выполнить или интерпретировать файл как доверенную страницу.
Безопаснее: хранить приватно, а отдавать через контролируемые ответы с безопасными заголовками.
Как не допустить, чтобы пользователь скачивал чужие файлы?
Используйте принцип по умолчанию отказано и проверяйте доступ при каждом скачивании или просмотре.
Практические правила:
- Каждая запись о файле должна иметь владельца (пользователь/организация) и назначение (avatar, invoice и т.д.)
- При чтении/скачивании проверяйте, имеет ли запросивший права на конкретный файл
- Не полагайтесь на правила вида «всё в /uploads/ безопасно»
- Доступ поддержки должен быть временным и логироваться (выдать доступ к одному файлу и автоматически истечь)
Большинство реальных багов — это простые «я вижу чужой файл» ошибки.
Как валидировать тип файла, не полагаясь на имя или Content-Type?
Не доверяйте расширению файла или заголовку Content-Type от клиента. Валидируйте на сервере:
- Используйте allowlist форматов для каждой фичи (JPEG/PNG для аватаров, PDF для квитанций)
- Определяйте тип на сервере и проверяйте magic bytes (сигнатуры файлов)
- Переименовывайте файлы при сохранении, используя случайный ID; оригинальное имя храните только как метаданные
- Блокируйте рискованные форматы, которые вам не нужны (особенно HTML, SVG и скриптоподобное содержимое)
Если байты не соответствуют разрешённому формату — отклоняйте загрузку.
Какие лимиты ставить, чтобы избежать DoS через загрузки?
Отказы и простои чаще всего вызваны банальными злоупотреблениями: слишком много загрузок, гигантские файлы или медленные соединения, которые занимают ресурсы.
Рекомендации:
- Устанавливайте максимальные размеры по фиче (аватар маленький, документ — больше)
- Применяйте ограничения на нескольких уровнях (приложение + reverse proxy + таймауты)
- Введите лимиты частоты по пользователю и по IP, для анонимных запросов — строже
Относитесь к каждому байту как к затратам и к каждому запросу как к потенциальной атаке.
Стоит ли использовать подписанные URL и какой безопасный дефолт?
Да, но осторожно. Подписанные URL дают возможность браузеру загружать/скачивать прямо в хранилище, не делая бакет публичным.
Хорошие практики:
- Писать write‑URL краткоживущими (обычно 1–5 минут)
- Привязывать URL к одному конкретному ключу объекта, а не к папке
- Выдавать URL только после проверки прав
- Логировать, кто и зачем запросил URL
Прямая загрузка в хранилище снижает нагрузку на API, но делает критичными области ограничения и сроки жизни URL.
Какой поэтапный безопасный поток загрузки можно реализовать?
Самый безопасный шаблон:
- Создайте запись загрузки со статусом
pending - Загружайте байты в приватное место
- На сервере проверьте размер и тип (magic bytes)
- Просканируйте (обычно асинхронно) и поставьте статус
cleanилиquarantined - Разрешайте скачивание/превью только при статусе
clean
Это предотвращает случайное распространение файлов, которые ещё не проверены.
Нужны ли мне вообще антивирусы и как выглядит «базовое сканирование»?
Сканирование полезно, но не даёт абсолютной гарантии. Используйте его как дополнительную защиту.
Практический подход:
- Сначала карантин: не делайте ссылки публичными, пока файл не проверен
- Для масштабирования сканируйте асинхронно и показывайте пользователю статус «обработка»
- Если сканирование упало или истекло время — считайте файл подозрительным и не выдавайте ссылок
- Если вы поддерживаете архивы, добавьте защиты от «zip bomb» и больших распаковок
Политика должна быть строгой: «не просканирован» ≠ «доступен».
Как безопасно отдавать загруженные файлы (заголовки, домены, скачивание)?
Отдавайте файлы так, чтобы браузер не интерпретировал их как страницу.
Полезные дефолты:
- Для документов используйте
Content-Disposition: attachment - Ставьте безопасный
Content-Type, выбранный сервером (илиapplication/octet-stream) - Используйте непрозрачные ключи объектов в URL, а не имена пользователей
- По возможности используйте отдельный домен для загрузок
Это снижает риск, что загруженный файл превратится в фишинговую страницу или выполнит скрипт.