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

Почему загрузки так быстро превращаются в проблему
Загрузки кажутся простыми, пока ими не начинают пользоваться реальные люди. Форма, рассчитанная на PDF, скоро получает фото с телефона, скриншоты, ZIP‑пакеты, большие видео и экспорт из старых офисных программ. Разрыв между тем, что вы ожидаете, и тем, что присылают пользователи, — место, где начинаются проблемы.
Размер быстро становится проблемой. Один большой файл может занять рабочий процесс на минуты, заполнить временную папку и толкнуть другие запросы в тайм‑ауты. Если несколько людей сделают то же самое одновременно, диски тихо заполняются, а приложение замедляется непонятными способами.
Имена создают другой тип хаоса. Пользователи присылают файлы «map scan final.jpg», «map scan final(1).jpg» и «final FINAL.jpg». Они выглядят безобидно, но могут скрывать дубликаты, странные символы, поддельные расширения или тексты, похожие на пути, которые ломают логи и утилиты.
Малые команды обычно замечают проблему слишком поздно. Тикетов в поддержку становится много. Кто‑то не может найти нужный документ клиента, другой скачал неправильную версию, третий спрашивает, почему выросли расходы на хранение. Файлы остаются, но доверия к ним уже нет.
Очистка обычно терпит неудачу по одной причине: никто за неё не отвечает. Команды хранят каждую загрузку «на всякий случай», включая неудачные импорты, старые сканы и тестовые файлы со staging. Несколько месяцев спустя никто не знает, какие файлы важны, кто их загрузил и что можно безопасно удалить.
Именно поэтому системы загрузки быстро превращаются в беспорядок. Причина чаще всего не в атаке, а в нормальном поведении: люди загружают больше, загружают странные файлы и оставляют намного больше данных, чем предполагал первый релиз фичи.
Решите, что пользователи действительно могут загружать
Начните с цели каждого файла. Это решение даёт больше пользы для безопасности загрузок, чем длинный список правил, добавленных позже.
Если пользователям нужны только счета — принимайте PDF. Если нужны фото профиля — принимайте JPG, PNG или WebP. Каждый дополнительный тип файлов добавляет риск, работу для поддержки и захламление хранилища.
Простой вопрос помогает: «Какое бизнес‑действие требует этот файл?» Если вы не можете ответить в одном предложении, пока не разрешайте загрузку. Случайные ZIP‑архивы, старые офисные форматы, исполняемые файлы и исходники дизайна часто проскальзывают, потому что в начале не приняли чёткое решение.
Держите отдельные правила для каждой области загрузки. Аватары и фото товаров требуют одну политику. Контракты, счета и отчёты — другую. Архивы стоит блокировать, если команде не нужен массовый импорт или экспорт. Внутренние админ‑загрузки должны иметь более жёсткий доступ, чем публичные формы.
Не оставляйте эти правила только в коде. Выпишите, кто может загружать, какие типы файлов разрешены, куда попадает каждый файл и кто сможет его прочитать позже. Форма поддержки клиентов и внутренняя панель финансов не должны делить одну и ту же политику загрузок.
Небольшой пример делает идею очевидной. Если клиентский портал собирает документы личности, разрешите PDF и распространённые форматы изображений, отправьте их в приватный путь хранения для этого клиента и блокируйте архивы. Если маркетинговая страница принимает логотипы — принимайте только изображения и храните их в отдельном месте.
Свободные правила загрузки быстро создают беспорядок. Чёткие правила держат хранилище чище, упрощают аудит доступа и делают будущие изменения безопаснее.
Задайте лимиты размера и скорости заранее
Хорошая безопасность загрузки начинается с скучных ограничений. Пропустите их, и один пользователь может отправить 2 ГБ видео в форму, которая нуждалась лишь в PDF. Приложение сожжёт CPU, память, пропускную способность и место на диске, пока кто‑то не заметит.
Устанавливайте жёсткий предел для каждой формы вместо одного глобального лимита. Фото профиля, счёт и CAD‑файл не должны иметь одно и то же правило. Малые и понятные лимиты также помогают пользователям — форма сразу говорит, что подходит, и отклоняет неправильные файлы.
Простая настройка часто работает: фото профиля — 5 МБ, PDF‑документы — 10–20 МБ, вложения в поддержку — не более трёх файлов на запрос, а массовые импорты обрабатываются в отдельном потоке с отдельными лимитами. ZIP‑файлы лучше блокировать, если продукт реально не требует их.
Ограничения по скорости важны так же, как и по размеру. Лимитируйте, сколько файлов пользователь может отправить в минуту, час и день. Ограничьте файлы на запрос. Если вы разрешаете 50 файлов в одном вызове, кто‑то попробует отправить 50 крупных, и воркеры задохнутся вместе.
Отказываетесь быстро. Проверяйте размер запроса ещё до того, как приложение начнёт сканирование на вредоносное ПО, определение типа, генерацию миниатюр, OCR или запись в базу. Поставьте первый шлюз на краю, затем дублируйте проверки в приложении. Двойная проверка дешева и спасает много проблем.
Архивные бомбы требуют отдельного отношения. Маленький сжатый файл может развернуться в нечто огромное. Если продукту не нужны архивы — отклоняйте их. Если нужны — распаковывайте в изолированном процессе с жёсткими ограничениями по развернутому объёму, количеству файлов и времени выполнения.
Для платных планов или общих рабочих пространств отслеживайте общий объём хранения на аккаунт. Пользователь, загрузивший 1 000 мелких файлов, может создать ту же проблему, что и один гигантский загруз. Запишите лимиты для каждой формы и применяйте их до начала дорогостоящей обработки.
Используйте безопасные имена и стабильные ID
Имя файла никогда не должно решать, где файл хранится или как приложение найдёт его позже. Генерируйте собственный ID для каждой загрузки и используйте его как ссылку в хранении. Это предотвращает перезапись файлов, если два человека загрузили «invoice.pdf» в один день.
Оригинальное имя всё ещё важно, но оно должно быть в метаданных. Сохраняйте его для отображения, поиска и подсказок скачивания. Не используйте его в качестве пути хранения, ключа в базе данных или публичного идентификатора.
Имена от пользователей нужно очищать перед сохранением. Удаляйте управляющие символы, обрезайте пробелы, сводите повторяющиеся точки и рубите очень длинные имена до разумной длины. Оставьте читаемое отображаемое имя, но простое. «march‑invoice.pdf» гораздо удобнее, чем 240‑символьный мусор с символами, которые инструменты могут неправильно интерпретировать.
Браузеры, мобильные приложения и drag‑and‑drop могут присылать подсказки папок или фальшивые локальные пути. Игнорируйте их. Сервер должен решать финальное место хранения по своим правилам, а не по тому, что клиент прислал.
Расширения обрабатывайте так же. Если файл говорит, что он .jpg, но проверка типа показывает PDF, доверьтесь обнаруженному типу. Переименуйте объект в хранилище так, чтобы он соответствовал проверенному типу, а оригинальную метку храните в метаданных, если хотите её показывать.
Достаточно простого реестра: сгенерированный ID файла, очищённое оригинальное имя, обнаруженный MIME‑тип, сохранённое расширение и метка времени загрузки. Это скучно, но именно скучные правила чаще всего работают годами.
Сканируйте файлы и проверяйте тип
Имя файла почти ничего не говорит. «invoice.pdf» может быть настоящим PDF, переименованным исполняемым файлом или повреждённым архивом. Для безопасности загрузок доверяйте сигнатуре файла и типу, который определил сервер, а не расширению или MIME‑типу от браузера.
Когда файл поступил, поместите его в приватную временную область и пометьте как pending. Не публикуйте его, не парсите и не запускайте изменение изображений или документы в обработку пока не проверите. Сначала убедитесь, что бинарная сигнатура соответствует разрешённому типу, затем запустите сканирование на вредоносное ПО.
Если проверки расходятся — остановите поток. Файл, который заявляет JPEG, но начинается с ZIP‑заголовка, не должен продвигаться дальше. Некоторые несоответствия приходят от невинных ошибок пользователя, но система должна считать их подозрительными до отклонения или ручной проверки.
Карантин держит остальное хранилище чище. Храните подозрительные загрузки в отдельном месте с жёстким доступом, коротким сроком хранения и без публичного доступа. Это даёт пространство для изучения проблемы, не смешивая плохие файлы с принятыми.
Некоторые форматы требуют изоляции даже после прохождения сканера. PDF, офисные документы, архивы и медиафайлы могут вызвать ошибки парсеров или скрывать вложенное содержимое. Парсите их в отдельном воркере или песочнице с жёсткими лимитами на CPU, память и время выполнения. Если воркер падает или доходит до тайм‑аута, отклоняйте файл и сохраняйте причину.
Ведите аудит для каждой загрузки. Логируйте ID загрузки, оригинальное имя и имя объекта в хранилище, обнаруженный тип, расширение и результат проверки сигнатуры, результат сканирования и версию сканера, а также итоговое действие: accepted, quarantined или rejected.
Эти записи экономят время, когда поддержка спрашивает, почему документ исчез или почему файл не стал доступен. Простой и аккуратный пайплайн обычно работает лучше, чем хитрый, который сначала принимает, а потом проверяет.
Держите объектное хранилище опрятным
Объектное хранилище пачкается, когда команды используют пути как вторую базу данных. Они кладут в имя пользователя, статус документа и другие меняющиеся детали, а потом месяцы чистят этот беспорядок. Держите пути скучными, короткими и предсказуемыми.
Сначала разделите по окружению. Продакшен, staging и тестовые загрузки не должны делить один бакет или верхний префикс. Также разделяйте публичные и приватные файлы. Публичный аватар и подписанный контракт не должны жить рядом, даже если приложение загружает оба.
Простая схема работает хорошо: группируйте объекты по клиенту, функции и дате, а в конце используйте стабильный ID файла. Это даёт структуру для поиска, применения правил и архивирования данных без лабиринтов. Если путь имеет семь уровней и три специальных случая, кто‑то положит файл не туда.
Храните реальные метаданные в базе. Сохраняйте оригинальное имя файла, размер, обнаруженный тип, контрольную сумму, владельца, статус сканирования и дату удаления. Путь должен указывать, куда файл принадлежит, а не нести всю бизнес‑логику.
Хороший пример словами: приватный счёт для клиента 482, загруженный в апреле, попадает в приватную продакшен‑зону под этого клиента и фичу, с сгенерированным ID в конце. Если тот же клиент завтра загрузит ещё один счёт с тем же оригинальным именем, ничего не столкнётся и не запутается.
Определите правила хранения и удаления
Если вы не решите, как долго файлы живут, они останутся навсегда. Это быстро дорого и усложняет безопасность, потому что старые файлы скапливаются там, куда никто не заглядывает.
Начните с ясных сроков для каждого класса файлов. Временные загрузки для черновиков, неудачных импортов и файлов, которые не завершили обработку, должны истекать быстро. Для многих продуктов 24 часа — 7 дней достаточно.
Бизнес‑документы нуждаются в других правилах. Храните счета, подписанные документы или доказательства от клиентов столько, сколько требует бизнес, контракт или закон. Если после этой даты никто не может объяснить, зачем файл хранится — удаляйте.
Простое расписание обычно достаточно. Временные загрузки исчезают через 24 часа. Неудачные задания по обработке — через 7 дней. Брошенные multipart‑загрузки — через 1 день. Для клиентских записей задайте фиксированную дату хранения и правило пересмотра.
Удаление требует лога. Сохраняйте, кто запросил удаление, кто его утвердил (если требуется), когда оно произошло и какой ID объекта удалён. Этот запись храните даже после удаления файла.
Soft delete помогает при ошибках пользователей, но окно восстановления держите коротким. 7–30 дней достаточны для большинства команд. Потом делайте окончательное очищение из объектного хранилища, превью, кэшей и индексов поиска.
Проверьте оба сценария перед запуском. Восстановите удалённый файл из бэкапа и проверьте, что правила доступа всё ещё применяются. Затем проверьте окончательное удаление и убедитесь, что файл, превью и метаданные полностью исчезают. Если удаление убирает только строку в БД, мусор останется, пока расходы на хранение или запрос соответствия не выявят проблему.
Постройте поток загрузки по шагам
Чистый поток загрузки держит плохие файлы подальше от постоянного хранилища и от любых задач, которые их затем читают. Не позволяйте браузеру выбирать финальный путь или записывать прямо в «живой» бакет. Сначала создайте серверный upload ID, свяжите его с пользователем и целью, и отслеживайте статус с первого запроса.
Один простой поток работает хорошо.
- Приложение запрашивает у сервера upload ID. Сервер фиксирует пользователя, разрешённый класс файлов, максимальный размер и статус, например «created».
- Перед приёмом файла сервер проверяет права, заявленный размер и допустимый тип. Если одна проверка не проходит — останавливайте процесс.
- Сохраняйте сырой файл в приватной временной зоне, например во временном бакете или префиксе. Делайте его невидимым для публичного доступа и других сервисов.
- Там запустите сканирование на вредоносное ПО и инспекцию файла. Подтвердите реальный тип по содержимому, а не только расширение, и не пускайте downstream‑задачи до завершения проверок.
- Если файл проходит — переместите его в финальное место и отметьте как «ready». Если нет — отметьте «rejected» и сохраните короткую причину.
Эти статусы важны больше, чем многие думают. «Upload failed» ничего не говорит поддержке. «Rejected: file too large» или «Rejected: malware detected» даёт понятный ответ и экономит время.
Небольшой пример: если клиент загружает PDF на проверку, сначала храните его под временным путём по upload ID, а не по оригинальному имени. После сканирования перемещайте в финальную структуру по стабильным ID: customer ID и document ID. Это упрощает очистку и снижает риск случайного доступа.
Реальный пример с документами клиента
Небольшой SaaS для биллинга часто требует два типа загрузок: счета от клиентов и файлы личности для проверки аккаунта. Оба чувствительны, но сотрудникам нужен быстрый доступ, пока аккаунт активен. Здесь безопасность загрузки перестаёт быть теорией и начинает влиять на ежедневную работу.
Чистая настройка начинается с приватной intake‑зоны. Когда пользователь загружает файл, приложение не кладёт его прямо в основную папку клиента. Оно сохраняет файл в intake‑бакете с короткоживущей записью, сгенерированным ID и без публичного доступа. Сотрудники продаж не могут открыть его сразу, как и клиент по угаданному URL.
Бэкграунд‑воркер делает скучную, но необходимую работу. Он сканирует файл на вредоносное ПО, сверяет реальный тип с разрешёнными, и заменяет оригинальное имя безопасным внутренним именем, например accountID плюс случайный токен. Если файл проходит — воркер перемещает его в долгосрочное хранилище для этого клиента и устанавливает дату хранения.
Доступ привязан к статусу аккаунта. Сотрудники продаж могут открывать документы для активных аккаунтов, потому что им это нужно для завершения сделок. Если аккаунт закрыт или триал истёк, приложение скрывает временные документы и удаляет их по расписанию.
Обычная политика: хранить загрузки триала 30 дней, счета — по требованиям финкоманды, неудачные или непроверенные загрузки — 24 часа, и логировать каждый просмотр, скачивание и удаление.
Такой поток держит хранилище чище, ограничивает экспозицию и экономит время поддержки при поиске старых файлов.
Ошибки, которые повторяются
Команды обычно создают проблемы загрузок, выбирая быстрый путь. Форма принимает файл, сервер сохраняет его — и все переходят к другому таску. Через полгода никто не знает, какие файлы безопасны, какие приватные и почему диск заполнен.
Первая ловушка — доверие. Файл «invoice.pdf» может быть исполняемым или скриптом с фальшивым расширением. Браузеры шлют MIME‑типы, которые полезны, но не доказывают реального типа. Хорошая безопасность загрузки начинается с проверки реального типа на сервере и трактовки пользовательского ввода как недоверенного.
Ещё одна ошибка — сохранять всё на диске приложения и оставлять там навсегда. Это работает в ранних тестах, потом ломает бэкапы, деплои и масштабирование. Если один сервер умрёт, файлы могут пропасть. Если логи и загрузки делят диск, всплеск больших загрузок навредит всему приложению.
Использование оригинального имени как пути тоже создаёт проблемы. Люди каждый день загружают файлы с одними и теми же именами. В некоторых именах есть пробелы, странные символы или трюки с путями. Храните оригинал как метаданные, а файл — под стабильным внутренним ID.
Ещё несколько проблем повторяются: временные загрузки не удаляются после неудачных форм, приватные документы лежат в том же бакете, что публичные изображения, старые экспорты и сканы остаются, потому что у них нет владельца по очистке. Поддержке сложно понять, какой файл к какой записи относится.
Простой пример: команда хранит фото профиля, подписанные контракты и CSV‑экспорты в одном месте. Вскоре правило для публичных изображений откроет приватные файлы, и нельзя будет применить одно правило хранения, не повредив другое.
Разделяйте файлы по назначению, держите приватное и публичное хранилище отдельно и применяйте правила хранения к временным данным тоже. Временные данные очень быстро превращаются в постоянный мусор.
Быстрые проверки перед запуском
День запуска — плохое время, чтобы выяснить, что одна загрузка может заполнить диск, забить очередь сканирования или остаться навсегда в временной папке. Тестируйте уродливые случаи целенаправленно: слишком большой файл, слишком много загрузок с одного аккаунта, файл, который никогда не закончится сканировать. Система должна отклонять, приостанавливать или истекать такие случаи без человеческого вмешательства.
Краткий предпусковой обзор экономит кучу очистки потом. Проверьте, что одна плохая загрузка не съедает локальный диск, объектное хранилище или время воркеров. Убедитесь, что поддержка может искать по upload ID и видеть владельца, статус файла, результат сканирования и состояние удаления. Проверьте, что вы можете удалить файлы одного клиента, не затрагивая других. Проверьте явные правила доступа: кто может загружать, кто смотреть и кто удалять. Проверьте, что временные файлы истекают сами и неудачные загрузки не остаются.
Вид для поддержки важнее, чем думают многие. Если клиент говорит «я загрузил это час назад», поддержка должна быстро найти файл. Стабильный upload ID, отметки времени и простой набор статусов — received, scanning, ready, rejected, deleted — обычно решают задачу.
Удаление требует такой же тщательности. Если клиент уходит, вы должны точно знать, где лежат его файлы, какие производные файлы существуют и какие логи доказывают удаление. Если вы не можете ответить за минуту — схема хранения ещё слишком запутана.
Небольшая тренировка перед запуском даёт результат: загрузите три файла, отклоните один, удалите одного клиента и подтвердите, что все временные копии, превью и объекты исчезли там, где нужно.
Следующие шаги для системы, с которой можно жить
Система загрузки остаётся управляемой, если вы выпишете правила на одной странице, а не в десяти местах. Держите политику понятной и конкретной: какие типы файлов разрешены, максимальный размер, как вы переименовываете файлы, как присваиваете стабильные ID и когда удаляете старые загрузки. Если продукт, поддержка и инженеры используют один короткий документ, меньше исключений превратятся в постоянные проблемы.
Перед запуском протестируйте кейсы, которые обычно создают проблемы. Загрузите слишком большой файл. Загрузите файл с неверным типом и поддельным расширением. Отправьте один и тот же файл дважды. Прервите загрузку на полпути. Повторите после тайм‑аута и убедитесь, что система не создаёт дубликатов.
Эти тесты ловят скучные ошибки, которые заполняют хранилище, путают пользователей и создают месяцы очистки.
После релиза следите за реальным трафиком и его влиянием на счёт за хранение. Сканирования, фото и повторные версии одного документа быстро накапливаются. Смотрите рост бакетов, неудачные сканы на вредоносное ПО, брошенные загрузки и действительно ли задания очистки удаляют устаревшие файлы по расписанию. Если числа смещаются рано — исправьте структуру и правила до того, как бэклог заполнится краевыми случаями.
Если хотите второе мнение по потокам загрузки, структуре хранилища или заданиям очистки, Oleg Sotnikov на oleg.is работает как Fractional CTO и консультант по инфраструктуре и архитектуре ПО. Такой обзор полезен особенно пока система ещё маленькая, потому что маленькие проблемы загрузок редко остаются маленькими надолго.
Часто задаваемые вопросы
Why do uploads get messy so fast?
Они обычно возникают из нормального использования, а не из сложных атак. Люди загружают больше, более нестандартные форматы и много дубликатов — первичная версия фичи этого не учитывает, и хранилище быстро забивается, пока никто не занялся очисткой.
Which file types should I allow?
Разрешайте только те форматы, которые нужны для конкретного бизнес‑действия. Если форма нужна только для счетов — принимайте PDF; если только для фото профиля — JPG, PNG или WebP. ZIP файлы блокируйте, если они действительно не нужны.
How do I choose size and rate limits?
Устанавливайте ограничения для каждой формы отдельно, а не одно правило на всё приложение. Фото профиля — например 5 МБ, документы — 10–20 МБ. Ограничьте число файлов в запросе и скорость загрузок с аккаунта.
Should I use the original filename as the storage path?
Нет. Генерируйте собственный ID для хранения файла и используйте его как ссылку в хранилище. Оригинальное имя сохраняйте только как метаданные для показа и поиска, предварительно очистив от странных символов и подсказок путей.
Do I really need to scan and verify every upload?
Да, если файл может взаимодействовать с вашими системами или командами позже. Проверяйте реальный тип по содержимому, сначала поместите файл в приватную временную область и запустите сканирование на вредоносное ПО, прежде чем перемещать в долгосрочное хранилище.
Where should temporary uploads go?
В приватный временный бакет или временный префикс без публичного доступа. Храните там до завершения сканирования и проверки типа, затем перемещайте удачные файлы в финальное место и быстро удаляйте отклонённые или брошенные.
How should I organize object storage?
Держите пути короткими и простыми. Разделяйте продакшен и staging, публичные и приватные файлы. Группируйте по клиенту и фиче, а реальные метаданные (владелец, тип, контрольная сумма, срок хранения) храните в базе данных.
How long should I keep uploaded files?
Для каждого класса файлов своё время хранения. Временные загрузки и неудачные импорты — быстро удаляются. Счета и подписанные документы храните столько, сколько требуется по бизнесу или закону. Если никто не может объяснить причину хранения — удаляйте.
What upload statuses should I track?
Используйте простые статусы: created, scanning, ready, rejected, deleted. Это даёт поддержку понятный ответ и помогает быстро понять, где остановился процесс.
What should I test before launch?
Прогоняйте «уродливые» кейсы перед запуском: слишком большой файл, много файлов одновременно, прерванная загрузка. Система должна автоматически отвергнуть, приостановить или истечь по срокам такие случаи без участия человека.