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

Загрузка файлов часто становится первым инцидентом, потому что она пересекает границу между пользователем и сервером. Пользователь приносит "свой" контент, а приложение по привычке начинает ему доверять: сохраняет, индексирует, показывает другим людям, иногда даже обрабатывает (превью, распаковка, конвертация). Этого достаточно, чтобы маленькая удобная фича превратилась в дыру.
Потери обычно выглядят прозаично. Утечки случаются, когда файлы лежат в общем бакете или отдаются по угадываемому пути. XSS появляется, когда вы показываете загруженный HTML или SVG как страницу, или отдаете файл с неправильным Content-Type, и браузер начинает исполнять содержимое. Переполнение диска и счета за хранилище прилетают, когда нет лимитов размера, квот и контроля количества загрузок. Отдельный риск - вредоносные вложения: их могут разослать через ваш сервис другим пользователям, а иногда и попытаться заставить бэкенд "проглотить" опасный формат.
"Безопасная загрузка файлов" на практике держится на двух простых идеях: не доверять файлу и не доверять имени файла. Имя может содержать странные символы, попытки обхода путей, совпадать с системными именами. Расширение и заявленный тип тоже ничего не доказывают: файл с .jpg может оказаться чем угодно.
Защита почти всегда получается многослойной: проверка на входе (размер, тип, количество, скорость), безопасное хранение (не рядом с кодом, уникальные имена, изоляция по пользователям), безопасная выдача (правильные заголовки, запрет исполнения, контроль доступа). Часто добавляют временный доступ через подписанные URL вместо вечных публичных ссылок, а также сканирование и карантин для подозрительных файлов.
Если вы собираете приложение на платформе вроде TakProsto, эти правила все равно применимы: автоматизация ускоряет разработку, но политика доступа, лимиты и способ выдачи файлов остаются ответственностью продукта.
Безопасная загрузка файлов часто ломается не из-за сложных атак, а из-за простых допущений: "проверим расширение и хватит". Мини-модель угроз помогает заранее решить, какие проверки обязательны, а какие можно отложить.
Обычно есть три класса нарушителей: анонимы, которые пытаются залить что угодно и найти слабое место; обычные пользователи, которые не обязательно злые, но будут загружать огромные файлы, дубликаты или "не то"; инсайдеры (или взломанные аккаунты), у которых уже есть доступ и больше шансов добраться до чужих данных.
Цели у них тоже приземленные:
Поверхности атаки обычно три. Первая - форма и API загрузки: обход лимитов, параллельные загрузки, неправильный Content-Type. Вторая - хранилище: предсказуемые имена, общий бакет, отсутствие изоляции по пользователям, возможность перезаписи. Третья - выдача и просмотр: если файл открывается в браузере как HTML или SVG, он может выполниться в контексте вашего домена.
Главная мысль: безопасная загрузка файлов - это не одна проверка, а цепочка решений. Расширение и "разрешенные типы" полезны, но сами по себе не защищают от подмены содержимого, XSS при просмотре и утечек через права доступа.
Самый частый провал в теме "безопасная загрузка файлов" - не вирусы и не расширения, а архитектура. Если хранить загрузки рядом с кодом сайта и раздавать их как обычные страницы, файлы пользователей становятся частью приложения.
Есть два рабочих подхода: загрузка через ваш сервер или прямая загрузка в файловое хранилище.
Файл сначала приходит на backend. Вы проверяете права, размер и тип, сохраняете, пишете метаданные в базу и только потом отвечаете. Это проще контролировать и логировать: видно, кто, что и когда загрузил.
Минус в том, что вы платите трафиком и ресурсами сервера. Большие файлы легко забивают каналы, память и диски, если нет жестких лимитов и таймаутов.
Клиент загружает файл сразу в объектное хранилище (например, S3-подобное), а сервер выдает разрешение на конкретную операцию. Это быстрее и дешевле по трафику на backend.
Цена за скорость - необходимость аккуратно работать с подписанными URL и политиками доступа. Ошибка в сроке действия, путях или правах может открыть чужие файлы или дать возможность перезаписывать объекты.
Где хранить: держите загрузки в отдельном хранилище или хотя бы в отдельном бакете/префиксе, с отдельными учетными данными и минимальными правами. Не храните их в папке, откуда фронтенд и сервер раздают статику, и не смешивайте с исходниками.
Как отдавать: по умолчанию безопаснее выдавать файлы как скачивание, а не как "открыть в браузере". Когда файл открывается inline, браузер может выполнить активное содержимое или показать документ так, что он будет выглядеть частью вашего сайта. Если нужен предпросмотр, лучше делать его через контролируемый слой (например, конвертацию в безопасный формат) или через отдельный домен для раздачи.
Безопасная загрузка файлов начинается не с антивируса, а с понятного потока: кто загружает, куда именно прикрепляется файл, где он хранится и как потом выдавать доступ. Если этот поток зафиксирован, риск случайных дыр заметно падает.
Практичный базовый сценарий для небольшого сервиса и для продукта с кабинетами пользователей выглядит так:
Проверьте пользователя и цель загрузки. Убедитесь, что у него есть право прикреплять файл именно к этому объекту (заказу, задаче, профилю) и что объект существует.
Проверьте размер до приема данных и после. До загрузки ориентируйтесь на заявленный размер (например, Content-Length), а после сравните фактический размер с лимитом. Несовпадение или превышение - повод оборвать загрузку и пометить попытку.
Нормализуйте имя файла, но не доверяйте ему. Оригинальное имя храните только как метку для интерфейса. Для хранилища генерируйте безопасный ключ (например, UUID + дата) и не используйте путь от клиента.
Запишите метаданные в базу. Минимум: владелец, к чему прикреплено, оригинальное имя, безопасный ключ, размер, тип (как минимум по расширению), хеш (например, SHA-256) и статус. Статусы помогают отделить "принят" от "разрешен к выдаче": pending, uploaded, scanning, ready, rejected.
Верните клиенту понятный ответ. Обычно это id файла и текущий статус, плюс как получить доступ позже (например, через отдельный запрос на выдачу доступа). Не обещайте, что файл уже доступен, если он еще не прошел проверку.
Пример: пользователь загружает PDF к заявке. Бэкенд принимает файл, кладет его в хранилище под ключом вида 2026/01/<uuid>, в PostgreSQL сохраняет владельца и статус "scanning", а в интерфейсе показывается "файл принят, идет проверка". Такое разделение стадий часто спасает от первой неприятной истории.
Если вы делаете безопасную загрузку файлов, начните не с антивируса, а с простого правила: по умолчанию все приватно. Файл не должен быть доступен по публичному адресу только потому, что он успешно загрузился.
Проверяйте права на каждое действие, а не только на вход в раздел. Пользователь, который может открыть страницу проекта, не обязан автоматически уметь скачивать все вложения в этом проекте. У действий разный риск и разная логика.
Полезно мыслить не "файлами", а операциями: загрузить (кто может добавлять), посмотреть (кто видит факт существования и метаданные), скачать (кто получает содержимое), удалить или заменить (кто может убирать и менять версии).
Изоляция по арендаторам или командам должна быть жесткой. Не смешивайте идентификаторы в путях и не полагайтесь на угадываемые номера. Привязывайте файлы к tenant_id или team_id и проверяйте контекст при каждом запросе. Если есть команды и проекты, файл из команды A не должен быть доступен из команды B даже случайно, даже если кто-то подставил другой id.
Практический пример: сотрудник загружает договор в рабочее пространство команды. Если ссылка на скачивание строится только по file_id, другой пользователь сможет перебрать id и получить чужие документы. Если же скачивание всегда требует проверки пары (team_id, file_id) и ролей, перебор перестает работать.
Не забывайте про логи доступа. Записывайте минимум: кто скачивал, когда, откуда (IP или устройство), какой файл и в каком контексте (команда, проект). Эти события нужны не только для расследований, но и как ранний сигнал: массовые скачивания за минуту часто заметны по журналу раньше, чем по жалобам.
Для платформ командной разработки вроде TakProsto это особенно важно: один аккаунт может состоять в нескольких командах, и границы между ними должны оставаться непроницаемыми на уровне API, хранения и выдачи файлов.
Размер загрузки - это не только про экономию диска. Без жестких ограничений один запрос может съесть память, забить временный каталог, занять воркеры и положить приложение.
Ставьте лимиты на двух уровнях: на один файл и на весь запрос. В multipart-запросе "маленькие файлы" легко превращаются в гигантский запрос из сотен частей, поэтому ограничивайте общий размер тела и число частей.
Важно, чтобы ограничения срабатывали до того, как файл будет полностью принят сервером:
Квоты лучше считать по факту успешной записи, а не по заявленному размеру. Иначе злоумышленник накрутит учет, отправляя оборванные загрузки. Если у вас командные пространства, квоты на команду защищают от ситуации, когда один участник съедает весь общий лимит.
Отдельно продумайте временные данные. При загрузке файлы часто пишутся во временную папку или буферизуются. Настройте лимиты на размер временного каталога и время жизни временных объектов, а также прерывайте чтение сразу после превышения лимита.
Если лимит превышен, отвечайте понятной ошибкой: сколько можно и что сделать (уменьшить файл, разбить архив, очистить место). При этом важно безопасно удалить временный файл и закрыть поток, чтобы не оставлять хвосты на диске.
Пример: пользователь пытается загрузить 300 МБ логов, а лимит 50 МБ. Правильное поведение - отклонить запрос на раннем этапе, удалить временный кусок и не создавать запись "файл загружен" в базе. На платформах вроде TakProsto это особенно важно, чтобы один проект не выбил ресурсы у других.
Подписанный URL нужен, когда файл лежит в хранилище отдельно от приложения, а доступ должен быть временным и строго ограниченным. Вместо того чтобы открывать папку целиком или раздавать постоянный адрес, вы выдаете ссылку, где условия запроса подписаны секретом. Сервер проверяет подпись и решает, можно ли выполнить операцию.
Типичный пример: пользователь загрузил договор, и вы хотите дать ему скачать файл только после входа в аккаунт. Приложение проверяет права, а затем выдает подписанную ссылку на скачивание, действующую несколько минут.
Срок жизни делайте коротким и понятным: минуты, а не дни. Это снижает ущерб, если ссылку переслали в чат или она утекла из логов. Если нужно скачать позже, проще выдать новую ссылку после повторной проверки прав.
Смысл подписи не только в "путь + срок", а в том, чтобы запретить подмену условий запроса. Обычно в подпись включают:
Если вы выдаете подписанный URL на загрузку, подпишите лимит размера и запретите смену пути. Иначе злоумышленник сможет залить гигабайты или перезаписать чужой объект.
Подписанные ссылки живут до истечения срока, поэтому отзыв нужно продумать заранее:
На практике удобно хранить файл по неугадываемому ключу, а подписанные URL выдавать только через бэкенд, где проверяются права и статус. В TakProsto это обычно решают в Go-бэкенде, а фронт получает уже готовую краткоживущую ссылку.
Проверка загрузок на вредоносное ПО почти всегда нужна, даже если вы принимаете только "безопасные" форматы. Реальная проблема не в расширении, а в том, что внутри может быть что угодно: макросы, эксплойты, вложенные архивы, или файл, который ломает обработчик.
Синхронная проверка (в том же запросе) подходит только для маленьких файлов и простых сценариев. У нее два минуса: пользователю придется ждать, а сервер держит соединение и ресурсы. Часто это заканчивается таймаутами и подвисающими загрузками.
Асинхронная проверка обычно надежнее. Вы принимаете файл, кладете его в карантин, ставите задачу в очередь и сразу отвечаете статусом "на проверке". Пользователь видит процесс, а вы не рискуете уронить загрузку из-за долгого скана.
Карантин - это простое правило: файл нельзя скачать или открыть, пока он не отмечен как чистый. Даже если файл уже лежит в хранилище, выдача блокируется на уровне приложения (или вы храните его в отдельном бакете/папке, недоступной для раздачи).
Практичная схема статусов:
Пример: сотрудник загружает "счет.pdf". Файл сохраняется как pending и недоступен по ссылке. Через несколько секунд задача в очереди проверяет файл. Если clean, доступ открывается. Если suspicious, пользователь получает понятное сообщение, а файл остается закрытым.
Сохраняйте не только "чисто/не чисто", но и контекст, чтобы разбирать инциденты и спорные случаи.
Минимальный набор:
Если сканер временно недоступен, не открывайте доступ "на авось". Лучше оставить файл в failed и повторить проверку по расписанию, чем выдать непроверенное содержимое.
Даже если загрузка прошла через права, лимиты и проверку, проблемы часто начинаются на этапе выдачи: браузер может попытаться исполнить файл, а не просто скачать. Поэтому безопасная загрузка файлов - это еще и про то, как вы определяете тип и какие заголовки ставите.
Не доверяйте расширению. Пользователь легко загрузит invoice.pdf.exe или переименует HTML в JPG. Проверяйте хотя бы два сигнала: заявленный MIME (из запроса) и "магические байты" (сигнатуру первых байтов файла). Если они конфликтуют, лучше отклонить файл или пометить как небезопасный и отдавать только на скачивание.
Для встроенного просмотра в браузере опасны форматы, где возможен скрипт или активный контент. Обычно для inline-отдачи блокируют:
Если формат попадает в серую зону, безопаснее заставить браузер скачать файл.
Для небезопасных или неизвестных типов отдавайте файл с Content-Disposition: attachment; filename="...", чтобы браузер не пытался открыть его как страницу. Дополнительно полезно выставлять Content-Type: application/octet-stream для неизвестных типов и X-Content-Type-Options: nosniff, чтобы снизить риск MIME-sniffing.
Отдельная тема - изображения. Если вы показываете загруженные картинки другим людям (например, аватар), лучше пересохранить их на сервере в безопасный формат (JPEG/PNG/WebP) и, при необходимости, удалить метаданные. EXIF часто содержит геолокацию и модель устройства. Пример: пользователь загрузил "фото товара", а в EXIF остались координаты дома. Пересохранение решает это без лишних настроек для человека.
Принцип простой: только строго разрешенные типы показывайте inline, все остальное отдавайте как скачивание, даже если файл прошел проверку при загрузке.
Большинство инцидентов с загрузками начинается не со сложных атак, а с пары удобных решений, которые потом тяжело откатить.
Первая ошибка - доверять имени файла. Пользователь может прислать passport.jpg.exe, а логика возьмет "расширение после точки" или сохранит имя как есть. В итоге появляются странные имена, коллизии, обход фильтров, а иногда и проблемы при скачивании. Безопаснее генерировать свое имя (UUID) и хранить оригинальное отдельно как метаданные.
Вторая ловушка - складывать загрузки в публичную папку и раздавать по прямому пути. Как только файл доступен по адресу, контроль прав легко ломается: кто-то угадал путь, кто-то переслал ссылку, где-то закешировалось. Даже "секретная" структура папок редко остается секретной.
Третья ошибка - надеяться, что антивирус на сервере сам все проверит. Часто он не подключен к вашему потоку загрузки, не видит объекты в S3-совместимом хранилище или проверяет уже после того, как файл стал доступен. Проверка должна быть явным шагом, а выдача - только после результата.
Четвертая - забыть про временные файлы и незавершенные загрузки. Обрывы соединения, повторные попытки и зависшие части забивают диск и превращают лимиты в фикцию.
Пятая - выдавать доступ по предсказуемым ID без проверки прав. Если URL выглядит как /files/12345, его начнут перебирать. Проверка нужна на каждый запрос: кто пользователь, к какому объекту он просит доступ, и разрешено ли это.
Памятка, которая почти всегда помогает:
Если вы собираете приложение в TakProsto, заранее заложите эти правила в сценарии: так меньше шанс, что временное решение станет постоянным и опасным.
Перед релизом полезно пройтись по короткому набору проверок, чтобы загрузки не стали первым инцидентом, даже если пользователи будут ошибаться или намеренно тестировать границы.
Перед выкладкой проверьте основу:
В продакшене важно не только запрещать, но и быстро замечать аномалии. Минимальные операционные настройки обычно такие:
План улучшений на 30 дней можно держать простым. Неделя 1: стабилизируйте лимиты, права и подпись ссылок с коротким TTL. Неделя 2: добавьте карантин (файл недоступен до проверки) и асинхронную проверку на вредоносное содержимое. Неделя 3: введите квоты и регулярную очистку, настройте алерты. Неделя 4: прогоните негативные тесты (переполнение, подмена MIME-типа, скачивание чужого файла) и проверьте восстановление после сбоев.
Следующий шаг: зафиксируйте поток загрузки как короткий план и прогоните его на тестовом окружении. В TakProsto это удобно делать через Planning Mode: описать требования человеческим языком, быстро собрать прототип с хостингом, а затем спокойно экспериментировать со снимками и rollback, если правки сломают сценарий. Если вы ведете несколько проектов и команд, проще держать эти требования рядом с продуктовой логикой, а не в головах разработчиков. Для этого можно использовать takprosto.ai как место, где сценарии и ограничения фиксируются прямо в процессе сборки приложения.
Начните с двух правил: не доверяйте содержимому файла и не доверяйте имени файла. Дальше выстройте цепочку: проверка на входе (права, лимиты, тип), безопасное хранение (уникальные ключи, изоляция), безопасная выдача (контроль доступа, правильные заголовки, по умолчанию скачивание).
Потому что расширение и Content-Type легко подделать. Минимум, что стоит сделать:
photo.jpg.exeСохраняйте оригинальное имя только как подпись для интерфейса. Для хранения всегда генерируйте серверный ключ, например UUID + дата.
Дайте доступ только после проверки прав и статуса файла.
(team_id, file_id) и роль пользователя/files/12345 без строгой авторизации на каждом запросеСтавьте лимиты заранее, чтобы они срабатывали до полного приема данных:
После отказа удаляйте временные куски и не создавайте запись «файл загружен».
Если файлы большие или загрузок много, прямая загрузка в объектное хранилище обычно дешевле по трафику для backend. Если важнее контроль и простота — принимайте через сервер.
В обоих случаях держите загрузки отдельно от кода и статики, с минимальными правами доступа и изоляцией по пользователям/командам.
Подписанный URL нужен для временного и строго ограниченного доступа.
Практика:
Хороший базовый поток такой:
pending/scanning и не доступенclean или suspicious/failedcleanЕсли сканер временно не работает, оставляйте файл в карантине и повторяйте проверку, а не открывайте доступ «на авось».
По умолчанию безопаснее принуждать скачивание.
Content-Type: application/octet-streamContent-Disposition: attachment; filename="..."X-Content-Type-Options: nosniff, чтобы снизить риск MIME-sniffingДа, автоматизация ускоряет сборку, но политика доступа и ограничения задаете вы.
Что обычно стоит явно заложить в сценарии TakProsto: