Безопасная загрузка файлов: чек-лист для разработчика
Практика3 минРедакция Innovialabs
Коротко
Загруженный файл — это недоверенные данные, которые нельзя ни исполнять, ни отдавать как есть. Проверяйте тип по allowlist и сигнатуре, ограничивайте размер, переименовывайте, храните вне web root, перекодируйте изображения и сканируйте на вирусы.
Главное правило
Любой файл от пользователя — недоверенный ввод. Имя, расширение, Content-Type и содержимое контролирует тот, кто его загружает. Опасных сценариев три:
- исполнение кода на сервере, если файл попал туда, где веб-сервер его запускает (
shell.phpв/uploads/); - XSS и фишинг, если HTML или SVG отдаётся с вашего домена и открывается в браузере;
- отказ в обслуживании — огромные файлы, «бомбы» в архивах и картинках.
Ниже — чек-лист, который закрывает все три.
1. Тип файла
- Allowlist расширений, а не blocklist:
jpg,png,pdf— и ничего сверх нужного. Список «запрещённых» всегда неполон (phtml,phar,shtml…). - Берите расширение после последней точки и приводите к нижнему регистру.
photo.php.jpgиphoto.JPG— частые обходы. Content-Typeиз запроса не доказательство: его выставляет клиент. Проверяйте сигнатуру файла (magic bytes) библиотекой вродеlibmagicилиfile-type.- Расширение, MIME и сигнатура должны совпадать. Расхождение — отказ.
2. Размер и количество
- Лимит на уровне прокси (
client_max_body_sizeв Nginx), приложения и хранилища — чтобы большой файл отсекался до того, как съест память. - Для изображений проверяйте размер в пикселях, а не только в байтах: маленький файл может распаковаться в гигантскую картинку.
- Архивы распаковывайте с лимитом на итоговый объём и число файлов, а пути внутри архива проверяйте на
../(zip slip). - Ограничьте число загрузок на пользователя и в единицу времени.
3. Где хранить
- Вне web root или в объектном хранилище (S3-совместимом) с приватным бакетом. Веб-сервер не должен иметь возможности исполнить загруженный файл.
- Отдавайте файлы через обработчик с проверкой прав или через подписанные ссылки с ограниченным сроком жизни.
- Пользовательский контент лучше раздавать с отдельного домена (
usercontent.example.net): даже если там окажется скрипт, он не получит cookie основного сайта. - Права на скачивание проверяйте так же строго, как на любые данные — иначе получится IDOR через
/files/123.
4. Имя файла
- Генерируйте имя сами (UUID или хеш) и храните оригинальное имя в базе.
- Так закрываются path traversal (
../../etc/passwd), перезапись чужих файлов, спецсимволы и слишком длинные имена. - Оригинальное имя при отдаче экранируйте в
Content-Disposition, а в интерфейсе выводите как текст.
5. Изображения
- Перекодируйте картинки: декодировать и сохранить заново через библиотеку обработки. Это удаляет посторонние данные, «полиглоты» (файл, валидный сразу в двух форматах) и метаданные EXIF, включая геолокацию.
- Держите библиотеки обработки обновлёнными: у них регулярно находят уязвимости.
- Обработку выполняйте в отдельном воркере или контейнере с ограничением ресурсов и без лишних сетевых доступов.
6. Антивирус
- Сканируйте файлы (например, ClamAV) до того, как они станут доступны другим пользователям.
- Удобная схема: файл попадает в карантин со статусом «на проверке», асинхронный воркер сканирует, и только затем файл публикуется.
- Антивирус не заменяет пункты выше: он ловит известное вредоносное ПО, а не XSS в SVG.
7. SVG и HTML
SVG — это XML, внутри которого могут быть <script>, обработчики событий и внешние ссылки. Если SVG открывается с вашего домена, это хранимый XSS. Варианты:
| Вариант | Когда подходит |
|---|---|
| Запретить SVG | если без него можно обойтись |
| Конвертировать в PNG | для аватаров и превью |
| Санитизировать (например, DOMPurify на сервере) | если векторный формат нужен |
| Отдавать с отдельного домена как attachment | для хранения «как есть» |
То же относится к HTML, XML и PDF с активным содержимым.
8. Заголовки при отдаче
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'; sandbox
nosniff запрещает браузеру угадывать тип, а attachment заставляет скачать файл вместо открытия. Больше деталей — в OWASP File Upload Cheat Sheet.