Загрузка файлов без сюрпризов
Проверка размера и типа, хранение, публичные и приватные ссылки, вредоносные файлы и обработка изображений — что нужно сделать до первой загрузки.
Загрузка файлов в прототипе занимает десять строк: поле input type="file", запрос, сохранение в бакет, ссылка в базе. Работает с первого раза. А потом кто-то загружает файл на два гигабайта, HTML-страницу под видом аватарки или фотографию паспорта, которая открывается по ссылке любому, кто её угадал.
Каждый загруженный файл — это данные от незнакомца, которые ты хранишь и показываешь другим людям. Относиться к ним стоит соответственно.
Размер
Лимит размера ставится на каждом уровне: на прокси, в приложении и в правилах хранилища. Если файл грузится напрямую в хранилище по подписанной ссылке, лимит задаётся в условиях этой ссылки — иначе он не проверяется вообще.
Кроме лимита на файл, нужна квота на человека: сто файлов по допустимому размеру — тоже проблема, особенно если хранение оплачиваешь ты.
Тип
Расширение файла и заголовок Content-Type присылает клиент, и оба можно подделать. Тип определяется по содержимому — по первым байтам файла (сигнатуре), и сверяется со списком разрешённых. Список именно разрешённых: «всё, кроме .exe» не работает, потому что опасных форматов больше, чем ты вспомнишь.
Отдельно про SVG. Это не картинка, а XML, внутри которого может быть JavaScript. Открытый в браузере с твоего домена, он выполнится с доступом к твоим cookie. Если SVG не нужен — не принимай его. Если нужен — очищай или конвертируй в растр.
Имя и место
- Имя файла придумываешь ты. Случайный идентификатор, а оригинальное имя — отдельным полем в базе. Имя от пользователя в пути к файлу — это
../и перезапись чужих файлов. - Не в папку веб-сервера. Файлы лежат в объектном хранилище или вне каталога, который сервер раздаёт как статику. Иначе загруженный скрипт может оказаться исполняемым.
- С отдельного домена. Пользовательский контент лучше раздавать с домена, не связанного с основным приложением. Тогда даже пропущенный HTML-файл не получит доступ к сессии.
- С правильными заголовками. Для скачиваемых файлов —
Content-Disposition: attachment, чтобы браузер не открывал их как страницу, иX-Content-Type-Options: nosniff, чтобы он не угадывал тип сам.
Публичные и приватные ссылки
Публичная ссылка подходит для того, что и так видно всем: аватарки, картинки товаров. Всё остальное — документы, сканы, вложения в переписке, счета — хранится в приватном бакете и отдаётся по подписанной ссылке с коротким сроком жизни. Ссылку выдаёт сервер, предварительно проверив, что человек имеет право на этот файл.
Непредсказуемое имя файла — это не защита. Ссылки попадают в логи, в историю браузера, в пересланные письма, и после этого их уже не отозвать.
Вредоносные файлы
Если файлы, загруженные одним человеком, скачивают другие — вложения, документы, общие папки, — их стоит проверять антивирусом. Делай это в фоне: файл помечается как «на проверке» и недоступен другим, пока проверка не прошла. Многие облачные хранилища и сервисы умеют это из коробки.
Обработка изображений
Картинку, которую ты будешь показывать, лучше не хранить в исходном виде, а перекодировать.
- Метаданные. EXIF с телефона часто содержит координаты, где сделан снимок. Перекодирование их убирает — и это вопрос приватности твоих пользователей, а не только безопасности.
- Размер в пикселях. Файл в несколько килобайт может разворачиваться в изображение, которому не хватит памяти сервера. Проверяй размеры до декодирования и ставь потолок.
- Изоляция. Библиотеки обработки изображений регулярно получают исправления уязвимостей. Обновляй их и запускай обработку в отдельном процессе или задаче с ограничением памяти и времени.
Не забудь про удаление
Удалил аккаунт — удали и файлы. Заказ отменён — вложения не должны висеть в хранилище годами. Файлы без владельца — это и лишние расходы, и данные, которые ты обязан защищать, хотя уже не используешь.
SUBXCAT может проверить, как устроена загрузка файлов в твоём продукте, и привести её в порядок без смены хранилища.