Обеспечьте безопасные загрузки файлов в масштабе с подписанными URL, жесткой проверкой типов и размеров, сканированием на вредоносное ПО и правилами доступа, которые остаются быстрыми при росте трафика.

Загрузки кажутся простыми, пока не приходят реальные пользователи. Один человек загружает фото профиля. Потом одновременно загружают PDF, видео и таблицы тысячи пользователей. Внезапно приложение тормозит, растут расходы на хранение, и копятся заявки в поддержку.
Типичные сбои предсказуемы. Страницы загрузки подвисают или таймаутятся, когда сервер пытается принять весь файл вместо того, чтобы отдать эту работу объектному хранилищу. Права со временем утекают, и кто‑то угадывает URL файла и видит то, что не должен. «Безобидные» файлы приходят с вредоносным ПО или в хитро устроенных форматах, которые ломают последующие инструменты. Логи неполные, и вы не можете ответить на простые вопросы: кто, что и когда загрузил.
Вам нужно нечто скучное и надёжное: быстрые загрузки, понятные правила (разрешённые типы и размеры) и аудит, который облегчает расследования.
Самая жёсткая компромисс — скорость против безопасности. Если запускать все проверки до завершения загрузки, пользователи ждут и повторяют, что увеличивает нагрузку. Если откладывать проверки слишком далеко, небезопасные или неавторизованные файлы распространяются до обнаружения. Практичный подход — разделить саму загрузку и проверки, и держать каждый шаг быстрым и измеримым.
Также конкретизируйте, что вы называете «масштабом». Пропишите числа: файлы в день, пик загрузок в минуту, максимальный размер файла и где находятся пользователи. Регионы важны для задержки и правил приватности.
Если вы строите приложение на платформе вроде Koder.ai, полезно задать эти лимиты заранее — они определяют дизайн прав, хранилища и фонового процесса сканирования.
Прежде чем выбирать инструменты, четко представьте, что может пойти не так. Модель угроз не должна быть большим документом — это короткое общее понимание того, что нужно предотвращать, что можно детектировать позже и какие компромиссы вы готовы принять.
Атакующие обычно пытаются пробраться через несколько предсказуемых точек: клиент (подделка метаданных или MIME типа), граничный уровень сети (повторы, обход лимитов), хранилище (угадывание имён объектов, перезапись) и скачивание/превью (опасная отрисовка или кража файлов через шаренные ссылки).
Дальше сопоставьте угрозы с простыми контролями:
Загрузки сверх размера — самая простая форма злоупотребления. Они увеличивают расходы и замедляют реальных пользователей. Останавливайте их рано жёстким лимитом по байтам и быстрым отклонением.
Фейковые типы файлов — следующий риск. Файл с именем invoice.pdf может быть чем‑то другим. Не доверяйте расширениям или проверкам в UI. Проверяйте по реальным байтам после загрузки.
Вредоносное ПО другое. Обычно вы не можете просканировать всё до завершения загрузки, не испортив UX. Типичная схема — детектировать асинхронно, помещать подозрительное в карантин и блокировать доступ до прохождения сканирования.
Неавторизованный доступ часто наносит самый большой урон. Рассматривайте каждую загрузку и каждое скачивание как решение по правам. Пользователь должен загружать только в место, куда он имеет право писать, и скачивать только то, что ему разрешено.
Для многих приложений здравый v1‑политика выглядит так:
Самый быстрый способ обрабатывать загрузки — не делать сервер приложения участником «битового» пути. Вместо проксирования каждого файла через бэкенд, позвольте клиенту загружать прямо в объектное хранилище с короткоживущим подписанным URL. Бэкенд фокусируется на решениях и записях, а не на передаче гигабайтов.
Разделение простое: бэкенд отвечает на вопрос «кто может загрузить что и куда», а хранилище принимает данные файла. Это убирает типичную точку отказа: серверы приложение, которые одновременно авторизуют и проксируют файл, и под нагрузкой исчерпывают CPU, память или сеть.
Храните небольшую запись о загрузке в базе (например, PostgreSQL), чтобы у каждого файла был явный владелец и жизненный цикл. Создайте эту запись до начала загрузки, затем обновляйте её по мере событий.
Поля, которые обычно окупаются: идентификатор владельца и tenant/workspace, ключ объекта в хранилище, статус, заявленный размер и MIME, и контрольная сумма для верификации.
Обращайтесь с загрузками как с машиной состояний, чтобы проверки прав оставались корректными даже при повторах.
Практичный набор состояний:
Разрешайте клиенту использовать подписанный URL только после того, как бэкенд создаст запись requested. После подтверждения загрузки хранилищем переводите в uploaded, запускайте сканирование в фоне и открывайте доступ к файлу только после approved.
Начинается всё, когда пользователь нажимает «Загрузить». Приложение вызывает бэкенд, чтобы начать загрузку с базовыми деталями: имя файла, размер и назначение (аватар, счёт, вложение). Бэкенд проверяет права для этой конкретной цели, создаёт запись загрузки и возвращает короткоживущий подписанный URL.
Подписанный URL должен быть максимально узконаправленным: позволять единственную загрузку к одному точному object key, иметь короткое время жизни и чёткие условия (лимит размера, разрешённый content type, опционная контрольная сумма).
Браузер загружает напрямую в хранилище по этому URL. Когда загрузка завершена, браузер вызывает бэкенд для финализации. При финализации снова проверьте права (доступ мог измениться) и подтвердите, что реально попало в хранилище: размер, обнаруженный content type и контрольная сумма, если вы её используете. Сделайте финализацию идемпотентной, чтобы повторы не создавали дубликатов.
Затем пометьте запись как uploaded и запустите сканирование в фоне (очередь/джоб). В UI можно показывать «Обработка», пока идёт сканирование.
Доверие к расширениям — путь к тому, чтобы invoice.pdf.exe оказался в бакете. Относитесь к валидации как к повторяемому набору проверок, которые выполняются в нескольких местах.
Начните с лимитов по размеру. Включите максимум в политику подписанного URL (или условия pre-signed POST), чтобы хранилище могло отклонить большие загрузки ещё на входе. Повторно примените тот же лимит при записи метаданных на бэкенде, потому что клиенты всё ещё могут попытаться обойти UI.
Проверки типа должны базироваться на содержимом, а не на имени файла. Проинспектируйте первые байты файла (magic bytes), чтобы подтвердить соответствие ожиданиям. Реальный PDF начинается с %PDF, PNG — с фиксированной сигнатуры. Если содержимое не соответствует allowlist, отклоняйте даже при «правильном» расширении.
Держите allowlist конкретным для каждой функции. Аватар может разрешать только JPEG и PNG. Для документов — PDF и DOCX. Это снижает риск и упрощает объяснение правил.
Никогда не доверяйте оригинальному имени файла как ключу в хранилище. Нормализуйте его для отображения (удалите странные символы, обрежьте длину), но храните свой безопасный object key — например, UUID с расширением, назначенным после определения типа.
Сохраняйте контрольную сумму (например, SHA-256) в базе и сверяйте её позже при обработке или сканировании. Это помогает поймать повреждения, частичные загрузки или подмены, особенно при повторах под нагрузкой.
Сканирование важно, но оно не должно быть в критическом пути. Принимайте загрузку быстро, а затем держите файл заблокированным, пока скан не пройдён.
Создавайте запись загрузки со статусом вроде pending_scan. В UI файл может показываться, но он не должен быть доступен для использования.
Сканирование обычно запускается по событию создания объекта в хранилище, публикацией задания в очередь сразу после завершения загрузки или и тем и другим (как страховка).
Воркер сканирования скачивает или стримит объект, прогоняет сканеры и записывает результат в базу. Храните минимум: статус сканирования, версию сканера, временные метки и кто инициировал загрузку. Такой аудит упрощает поддержку, когда пользователь спрашивает: «Почему мой файл заблокирован?»
Не оставляйте плохие файлы среди чистых. Выберите политику и применяйте её последовательно: карантин с ограничением доступа или удаление, если файл не нужен для расследования.
Какую бы стратегию вы ни выбрали, формулируйте сообщение пользователю спокойно и конкретно. Подскажите шаги (перезагрузить, связаться с поддержкой). Оповестите команду, если в короткий период появляется много отказов.
Главное правило — строгий запрет на выдачу и превью: только файлы со статусом approved доступны для сервинга. Всё остальное возвращает безопасный ответ вроде «Файл всё ещё проверяется.»
Быстрые загрузки хороши, но если неверный человек может прикрепить файл к чужому workspace, это хуже, чем медленные ответы. Самое простое и сильное правило: каждая запись файла принадлежит ровно одному tenant (workspace/org/project) и имеет явного владельца или создателя.
Проверяйте права дважды: при выдаче подписанного URL и снова, когда кто‑то пытается скачать или просмотреть файл. Первая проверка останавливает неавторизованные загрузки. Вторая защищает, если доступ был отозван, URL утёк или роль пользователя изменилась после загрузки.
Принцип минимальных прав делает и безопасность, и производительность предсказуемой. Вместо одной широкой «files»‑права разделите роли: «can upload», «can view», «can manage (delete/share)». Многие запросы затем превращаются в быстрые проверки (пользователь, tenant, действие), а не в тяжёлую бизнес‑логику.
Чтобы предотвратить угаданные ID, избегайте последовательных идентификаторов в URL и API. Используйте непрозрачные идентификаторы и делайте ключи хранилища неугадываемыми. Подписанные URL — транспорт, а не ваша система прав.
Шаринг файлов часто приводит к замедлению и путанице. Рассматривайте шаринг как явные данные, а не как неявный доступ. Простой подход — отдельная запись шаринга, дающая пользователю или группе доступ к конкретному файлу с опциональным сроком истечения.
Когда говорят о масштабировании безопасных загрузок, часто фокусируются на проверках и забывают главное: перенос байтов — самое медленное. Цель — держать большие файлы вне путей вашего приложения, ограничивать повторы и не превращать проверки в неограниченную очередь.
Для больших файлов используйте multipart или chunked‑загрузки, чтобы ненадёжное соединение не заставляло начинать заново. Чанки также помогают задать ясные лимиты: общий максимум, максимальный размер чанка и максимальное время загрузки.
Задавайте таймауты и повторы на клиенте сознательно. Несколько попыток помогают реальным пользователям; бесконечные повторы взрывают расходы, особенно в мобильных сетях. Стремитесь к коротким таймаутам на чанк, небольшому числу повторов и жёсткому дедлайну для всей загрузки.
Подписанные URL ускоряют путь данных, но запрос на их создание остаётся горячей точкой. Защитите его, чтобы он оставался отзывчивым:
Задержка тоже зависит от географии. Держите приложение, хранилище и воркеры сканирования в одном регионе, когда это возможно. Если нужны региональные развертывания по требованиям соответствия, планируйте маршрутизацию заранее, чтобы загрузки не пересекали континенты. Платформы с глобальным развертыванием (как Koder.ai) могут размещать рабочие нагрузки ближе к пользователям, когда важна локальность данных.
Наконец, продумывайте и скачивания, а не только загрузки. Подавайте файлы через подписанные ссылки на скачивание и задавайте правила кэширования в зависимости от типа и уровня приватности. Публичные ресурсы можно кэшировать дольше; приватные квитанции — короткоживущие и с проверкой прав.
Представьте бизнес‑приложение, где сотрудники загружают счета и фото квитанций, а менеджер утверждает их для возмещения. Здесь проектирование загрузок перестаёт быть академическим: много пользователей, большие изображения и реальные деньги на кону.
Хороший поток использует понятные статусы, чтобы всем было ясно, что происходит, и чтобы рутинные задачи можно было автоматизировать: файл попадает в объектное хранилище и вы сохраняете запись, привязанную к пользователю/workspace/expense; фоновая задача сканирует файл и извлекает базовые метаданные (например, реальный MIME); затем элемент либо утверждают и он становится доступным в отчётах, либо отклоняют и блокируют.
Пользователям нужен быстрый и конкретный фидбек. Если файл слишком велик, покажите лимит и текущий размер (например: «Файл 18 МБ. Максимум 10 МБ.»). Если тип неверный, укажите допустимые форматы («Загрузите PDF, JPG или PNG»). Если сканирование не прошло, оставьте сообщение спокойным и полезным («Файл может быть небезопасен. Пожалуйста, загрузите новую копию.»).
Команде поддержки нужен след, который поможет отлаживать без открытия файла: upload ID, user ID, workspace ID, временные метки created/uploaded/scan started/scan finished, коды результата (слишком большой, несоответствие типа, скан провален, отказ в доступе), а также ключ хранилища и контрольная сумма.
Повторные загрузки и замены — обычное дело. Обрабатывайте их как новые загрузки, привязывайте к той же расходной операции как новую версию, храните историю (кто заменил и когда) и помечайте активной только самую новую версию. В Koder.ai это естественно моделируется таблицей uploads и таблицей expense_attachments с полем версии.
Большинство багов с загрузками — не хитрые уязвимости, а малые упрощения, которые превращаются в риск при росте трафика.
Дополнительные проверки не обязательно делают загрузки медленными. Разделите «быстрый путь» и «тяжёлый путь».
Делайте быстрые проверки синхронно (авторизация, размер, допустимый тип, лимиты скорости), а глубокую инспекцию и сканирование — в фоне. Пользователь может продолжать работать, пока файл переходит из «uploaded» в «ready». При использовании чат‑ориентированного конструктора вроде Koder.ai придерживайтесь той же идеи: делайте эндпоинт загрузки маленьким и строгим, а сканирование и постобработку — джобами.
Прежде чем выпускать загрузки, определите, что значит «достаточно безопасно для v1». Команды чаще попадают в беду, смешивая строгие правила (которые блокируют реальных пользователей) с отсутствием правил (которые приглашают злоупотребления). Начните с малого, но обеспечьте чёткий путь от «получено» до «разрешено для скачивания» для каждой загрузки.
Короткий предрелизный чеклист:
Если нужен минимум для MИВ (MVP), держите его простым: лимит размера, узкий allowlist типов, загрузка через подписанный URL и «карантин до прохождения сканирования». Добавляйте приятные функции позже (превью, больше типов, фоновые переработки), когда основной путь стабилен.
Мониторинг — то, что не даст «быстрому» превратиться в «таинственно медленное» по мере роста. Отслеживайте долю неудачных загрузок (клиентские vs сервер/хранилище), долю провалов сканирования и латентность сканирования, среднее время загрузки по бакетам размеров, отказы авторизации при скачивании и паттерны исходящего трафика хранилища.
Запустите небольшой нагрузочный тест с реалистичными размерами файлов и сетями из реального мира (мобильная сеть ведёт себя иначе, чем офисный Wi‑Fi). Исправьте таймауты и повторы до релиза.
Если вы реализуете это в Koder.ai (koder.ai), Planning Mode — удобное место, чтобы сначала спроектировать состояния загрузки и эндпоинты, а затем сгенерировать бэкенд и UI под этот поток. Снэпшоты и откаты помогут при регулировке лимитов и настройке правил сканирования.
Используйте загрузки напрямую в объектное хранилище через короткоживущие подписанные URL, чтобы ваши серверы не передавали байты. Бэкенд отвечает за решения об авторизации и за запись состояния загрузки, а не за поток данных.
Проверяйте дважды: при создании загрузки и выдаче подписанного URL, затем снова при финализации и при выдаче ссылки на скачивание. Подписанные URL — это транспорт, но приложение должно хранить и проверять права в записи файла и привязанности к tenant/workspace.
Рассматривайте запись загрузки как машину состояний, чтобы повторы и частичные ошибки не разрушали правила безопасности. Частая последовательность: requested, uploaded, scanned, approved, rejected. Скачивание разрешать только для approved.
Заложите ограничение по байтам в политику подписанного URL (или в условия pre-signed POST), чтобы хранилище могло отклонить слишком большие файлы. Повторно проверьте тот же лимит при финализации, опираясь на метаданные, сообщённые хранилищем.
Не доверяйте расширению или MIME, присланному браузером. Определяйте тип по реальным байтам файла после загрузки (например, magic bytes) и сверяйте с жёстким allowlist для той функции.
Не держите пользователя в ожидании сканирования. Принимайте загрузку быстро, помещайте файл в карантин, запускайте сканирование в фоне и разрешайте скачивание/превью только после чистого результата.
Выберите согласованную политику: помещать заражённые файлы в карантин и закрывать доступ или удалять, если они не нужны для расследования. Дайте пользователю спокойное и понятное сообщение и храните аудит‑данные для поддержки.
Никогда не используйте имя файла от пользователя или путь как ключ в хранилище. Генерируйте непредсказуемый object key (например, UUID) и храните оригинальное имя как метаданные для отображения после нормализации.
Используйте multipart/чанк‑загрузки, чтобы нестабильное соединение не заставляло начинать заново. Ограничьте число повторов, задайте разумные таймауты и общий дедлайн для загрузки.
Храните минимальную запись: владелец, tenant/workspace, object key, статус, временные метки, обнаруженный тип, размер и контрольная сумма (если используете). При использовании Koder.ai это легко вписывается в Go бэкенд и таблицы PostgreSQL с фоновыми задачами сканирования.