Multipart/form-data часто воспринимают как «транспорт для файлов»: браузер собрал части, сервер разобрал, дальше бизнес-логика. Но именно на стыке формата, стандартов и библиотек рождаются неожиданные эффекты. Атакующий играет на нюансах: дубликаты полей, спорная трактовка boundary, особые параметры в Content-Disposition, запутанные имена с «квадратными скобками». Итог — проверка проходит, а дальше в систему попадает совсем не то, что ожидалось. 
 
Что такое multipart/form-data и где у него слабые места
 	multipart/form-data разбивает содержимое на части (parts), которые разделены строкой-границей (boundary). У каждой части есть заголовки (обычно Content-Disposition с name и, при загрузке, filename; иногда Content-Type) и тело — либо текстовое значение, либо байты файла. 
 
Ошибки и расхождения чаще всего касаются:
- распознавания границ (поведение при лишних/отсутствующих CRLF, пробелах, вложенных границах);
- обработки параметров заголовков (filename,filename*, кавычки, экранирование, пробелы, кодировки);
- дубликатов полей (что делать, если пришло несколько role=...);
- интерпретации имён вроде user[role](для одних это вложенная структура, для других — просто строка);
- доверия к заявленному Content-Typeвместо проверки содержимого;
- ресурсных лимитов (много мелких частей, огромные заголовки, временные файлы).
Ниже — образец «сырых» данных запроса для ориентира:
POST /upload HTTP/1.1
 Host: example.test
 Content-Type: multipart/form-data; boundary=----AaB03x
 Content-Length: 348
 
 ------AaB03x
 Content-Disposition: form-data; name="title"
 
 Фото с отпуска
 ------AaB03x
 Content-Disposition: form-data; name="file"; filename="beach.jpg"
 Content-Type: image/jpeg
 
 <байты JPEG>
 ------AaB03x-- Что считать Multipart/Form-Data Injection
Под «инъекцией» здесь будем понимать любой способ сформировать тело multipart так, чтобы парсер сервера увидел структуру или значения не такими, как предполагает прикладной код. Цели — обойти валидацию, подменить ключевое поле, протащить файл с неожиданным именем/типом, истощить ресурсы парсера.
Ходы, которые используют чаще всего:
- Дубликаты имён — разные библиотеки по-разному решают, какое значение «главное»;
- Игра с границами — лишние CRLF, пробелы, «ступеньки» перед boundary, вложенные разделители;
- Параметры заголовков — filename*с RFC 5987, странные кавычки, пробелы вокруг «=»;
- Имена как «структуры» — user[role],files[], смешанные индексы;
- Несоответствие типа — заявлен одно MIME, по сигнатуре — другое;
- Нагрузка на парсер — множество крошечных частей, гигантские заголовки, глубокие «массивы» ключей.
PHP: особенности парсинга и характерные ловушки
 	В PHP разбор выполняет движок и SAPI: разработчик получает $_POST и $_FILES. Это удобно, но есть нюансы:
 
- Преобразование имён в массивы. Ключи вида user[role],files[]превращаются во вложенные структуры. Если глубина не ограничена, можно нарушить ожидания валидации.
- Дубликаты имен. Одно имя может стать списком значений; порядок не всегда очевиден и зависит от версии и SAPI.
- Имена файлов. Использовать «как прислали» — риск коллизий и path traversal. Файлы следует переименовывать, а «человеческое» имя хранить только как проверенные метаданные.
- Лимиты. post_max_size,upload_max_filesize,max_file_uploads,max_input_vars,max_input_time— без них сервер легко перегружается.
Пример подмены структуры через глубину ключей:
------AaB03x
 Content-Disposition: form-data; name="user[role]"
 
 user
 ------AaB03x
 Content-Disposition: form-data; name="user[role][is_admin]"
 
 1
 ------AaB03x-- 
 	Если в коде ожидали строку user[role], а пришла вложенная конструкция, проверка может не сработать, тогда как глубже по стеку значение используется как «истина». 
 
Python: что по умолчанию делают Flask/Django/FastAPI и другие
В Python нет единого поведения для всех фреймворков. Общая картина:
- Дубликаты. В Flask/Werkzeug и Django доступен список значений (getlist()). Но многие берут первое попавшееся значение черезget(), не осознавая стратегию выбора.
- Имена-«массивы». Большинство Python-стеков трактуют имя как строку, без автоматического «разворачивания» []. Это снижает магию, но перенос кода «из мира PHP» без переосмысления часто даёт ошибки.
- Кодировки параметров. filename*(RFC 5987) поддерживается неравномерно; подробности экранирования и кавычек — источник граничных багов.
- Буферизация. WSGI/ASGI-реализации по-разному буферизуют части; без ограничений легко устроить «медленный» отказ в обслуживании.
Техника атаки: короткие и наглядные примеры
Дубликаты поля: разные значения для проверки и для бизнес-логики
Отправляем одно имя дважды: «безопасное» для валидации, «опасное» — чтобы попасть в логику.
------AaB03x
 Content-Disposition: form-data; name="role"
 
 user
 ------AaB03x
 Content-Disposition: form-data; name="role"
 
 admin
 ------AaB03x-- 
 	В PHP это может стать массивом; во Flask/Django надо использовать getlist(), иначе выбор значения — вопрос реализации. Несогласованность даёт шанс «нажать нужную кнопку». 
 
Границы и пустые строки: когда парсер «теряет» часть
Лишние пустые строки или пробелы перед boundary могут привести к разному прочтению структуры.
Content-Type: multipart/form-data; boundary=----AaB03x
 
 ------AaB03x
 
 Content-Disposition: form-data; name="meta"
 
 ok
 ------AaB03x
 Content-Disposition: form-data; name="file"; filename="x.php"
 Content-Type: application/octet-stream
 
 
 ------AaB03x-- Один парсер воспримет пробелы как часть предыдущего тела, второй — как валидный разделитель. Валидация видит «только meta», а реальный файл просачивается ниже по стеку.
filename* против filename: кто главнее
 
 	Если фильтры ориентируются на filename, а библиотека — на filename*, имя можно навязать через RFC 5987:
 
Content-Disposition: form-data; name="file"; filename="safe.txt"; filename*=UTF-8''..%2F..%2Fvar%2Fwww%2Fhtml%2Fshell.php
 Правильная защита — полностью игнорировать пользовательские пути и сохранять файл под сгенерированным именем.
Распространённый случай при миграциях: user[role][0]
  ------AaB03x
 Content-Disposition: form-data; name="user[role][0]"
 
 admin
 ------AaB03x-- Для PHP это массив, а часть логики может рассчитывать на строку. Несоответствие ожиданий — удобная щель для обхода проверки.
Несовпадение MIME и реального содержимого
 	Заявить image/png в заголовке, но положить PDF в теле — трюк старый, но до сих пор рабочий там, где доверяют заголовку части, а не сигнатуре файла. 
 
Content-Disposition: form-data; name="avatar"; filename="me.png"
 Content-Type: image/png
 
 %PDF-1.7
 ... Много маленьких частей: DoS за счёт большого числа частей
Тысячи коротких частей с длинными заголовками, множество временных файлов, рост потребления CPU и диска — и приложение уже не отвечает. Если нет лимитов на количество частей и размеры, сценарий воспроизводится без изысков.
Как это тестировать: инструменты и подход
Поддерживайте рядом набор инструментов: удобнее проверять гипотезы, когда можно собрать «сырое» тело и посмотреть, что именно видит сервер.
- curl — базовая работа с multipart: дубликаты, явные boundary, «сырые» тела;
- Burp Suite — Repeater/Intruder для ручного редактирования и фуззинга;
- httpbin и webhook.site — чтобы увидеть, что реально ушло;
- Postman и HTTPie — сборка сложных форм и автотесты коллекциями.
 	Несколько приёмов с curl:
 
# Дубликаты полей: --form можно повторять
 curl -i -X POST https://target/upload 
   -F "role=user" 
   -F "role=admin" 
   -F "file=@beach.jpg;type=image/jpeg"
 
 # Явный boundary и «сырое» тело
 
 curl -i -X POST [https://target/upload](https://target/upload) 
 -H 'Content-Type: multipart/form-data; boundary=----AaB03x' 
 --data-binary @payload.txt Чек-лист защиты (короткая версия)
Свод правил, который закрывает основные дыры ещё до прикладной логики:
- Дубликаты. Для критичных полей — отказ (HTTP 400) при наличии дублей. Для остальных — явная стратегия (например, «берём первое, остальные логируем»).
- Белые списки имён полей, ожидаемых типов и допустимых значений.
- Имена файлов. Никогда не используйте клиентское имя для пути на диске. Сохраняйте под своим идентификатором (UUID), «оригинал» — только как метаданные после очистки.
- Проверка содержимого по сигнатурам (magic numbers) и размеру; MIME из заголовка — вторично.
- Лимиты: размер тела, количество частей, размер заголовков, глубина ключей. Для «тяжёлых» эндпоинтов — отдельные, более строгие настройки.
- Стриминг и backpressure: не держите большие файлы в памяти; пишите потоково на диск/облако.
- Кодировки и экранирование параметров Content-Disposition; некорректные — в отказ.
- Логи аномалий: фиксируйте дубликаты, пустые части, слишком длинные параметры, всплески 4xx.
- Регулярные обновления библиотек парсинга и отключение самописных «велосипедов».
PHP: практические настройки и приёмы
Начните с конфигурации и строгой обработки входящих данных:
- post_max_size,- upload_max_filesize,- max_file_uploads,- max_input_vars,- max_input_time— выставьте значения под вашу нагрузку и сценарии;
- upload_tmp_dir— отдельный том с квотами, без права исполнения, с мониторингом места;
- Переименовывайте файлы при сохранении, храните исходное имя только как очищенные метаданные;
- Ограничьте глубину «массивов» в именах ключей и допустимые шаблоны (files[], не более N элементов);
- Проверяйте содержимое через finfoи чтение «магических» байтов, не полагайтесь на$_FILES['type'];
- В фреймворках (Symfony/Laravel) используйте рекомендованные абстракции; не читайте php://inputнапрямую без необходимости.
Python: практические настройки и приёмы
Точки контроля — реверс-прокси, сервер приложений и код фреймворка:
- На уровне прокси (например, Nginx) задайте client_max_body_size,client_body_timeout, буферы заголовков;
- В приложении ограничьте размер тела и количество частей (мидлвари для Starlette/FastAPI, настройки Django);
- Работайте со списками значений (getlist()) и задайте политику по дублям;
- Сохраняйте файлы потоково и под своим именем, проверяйте сигнатуры;
- Используйте валидаторы схем (Pydantic/Marshmallow) уже после своей «санитарной» обработки входа;
- Разведите эндпоинты: загрузки — отдельно и с более жёсткими лимитами и логированием.
Политика по дубликатам: сформулируйте один раз и закрепите тестами
Самая частая причина инцидентов — не поведение парсера, а неопределённость. Для ключевых полей выберите одно из двух: запретить дубликаты или детерминированно обрабатывать (например, брать только первое). Опишите это в контракте API и добавьте автотесты с «грязными» примерами.
Мини-тесты: как зафиксировать правила в коде
Ниже — эскиз теста для ASGI-приложения: проверяем, что дубликаты критичного поля режутся на входе.
<!-- псевдокод: идею легко перенести на ваш стек -->
 def test_reject_duplicates(client):
     boundary = "----AaB03x"
     body = f"""------AaB03x
 Content-Disposition: form-data; name="role"
 
 user
 ------AaB03x
 Content-Disposition: form-data; name="role"
 
 admin
 ------AaB03x--"""
 r = client.post(
 "/upload",
 headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
 data=body.encode("utf-8"),
 )
 assert r.status_code == 400 
 	Аналогично можно проверить лимиты по размеру, количеству частей и корректность обработки filename*. 
 
Реверс-прокси и сервер: где удобно «отрезать лишнее»
Часть проблем проще решать до приложения — на уровне Nginx/Apache и сервера приложений:
- Nginx: client_max_body_size,client_body_timeout,client_body_buffer_size,large_client_header_buffers; отдельныйlocationдля загрузок с жёсткими лимитами;
- Apache: LimitRequestBody,LimitRequestFieldSize,LimitRequestFields;
- Uvicorn/Gunicorn: параметры воркеров и таймаутов; ограничение размеров через мидлвари/конфиг.
Что мониторить: быстрые индикаторы проблемы
Даже без явной атаки полезно видеть аномалии:
- всплеск числа частей на запрос и доли «пустых» частей;
- дубликаты имён у критичных полей (role,price,planи т.п.);
- необычно длинные параметры filename/filename*и частота нестандартных кодировок;
- рост 4xx на эндпоинтах загрузки, увеличение времени парсинга;
- рост использования диска в каталоге временных файлов.
Короткий FAQ
 	Нужно ли запрещать filename*? Нет. Просто не используйте его содержимое для пути на диске. Сохраняйте под своим именем, а оригинал храните как проверенные метаданные. 
 
Дубли полей — всегда ошибка? Не всегда. Для чекбоксов и тегов список значений — норма. Важно отделять такие поля от критичных и применять разные политики.
Достаточно ли смотреть на расширение файла? Нет. Проверьте сигнатуру и лимиты, а ещё — поведение downstream-процессоров (например, генераторов превью).
Стоит ли писать свой парсер multipart? Нет. Поддерживать все граничные случаи трудно; безопаснее обновлять и настраивать зрелые библиотеки.
Полезные ссылки и справочники
- Спецификация формата: RFC 7578 (multipart/form-data), исторический RFC 2388.
- Параметры с кодировками:  RFC 5987 (filename*и др.).
- PHP: Загрузка файлов.
- Werkzeug/Flask: MultiDict.
- Django: File Uploads.
- Starlette/FastAPI: Requests & File Uploads, Request Files.
- Инструменты: curl, Burp Suite, HTTPie, httpbin, webhook.site.
- OWASP: Unrestricted File Upload.
Вместо вывода: договоритесь с собой и с кодом
Инъекции в multipart держатся на неявных предположениях. Снимите их: определите политику по дублям, зафиксируйте допустимые имена и типы, включите лимиты и журналирование аномалий, добавьте несколько «грязных» автотестов. Это закрывает большую часть уязвимостей ещё на пороге. Остальное — вопрос регулярных обновлений библиотек и аккуратного обращения с файлами пользователей.
 
		        
		        
		