Обсудить проект

Mass Assignment: один параметр — рут на проде

18.08.2026

Сегодня тема — не баг, а прямо-таки философия ленивых бэкендеров: «пусть модель сама разберётся, что там во входящем JSON». Спойлер: разбирается она плохо. Погнали ковырять Mass Assignment.

Что вообще происходит

Mass Assignment — это когда фреймворк на автомате мапит все поля из тела запроса прямо в объект модели, без allowlist’а разрешённых атрибутов. Разработчик думал, что клиент пришлёт username, password, email — а клиент присылает ещё и isAdmin, role, balance, потому что почему бы и нет.

Классический пример из OWASP WSTG — форма регистрации:

POST /createUser
username=bob&password=supersecretpassword&email=bob@domain.test&isAdmin=true

Если контроллер биндит объект автоматически (Rails ActiveRecord, Spring @ModelAttribute, Django ModelForm без fields, .NET [Bind] без ограничений) — поздравляю, Боб только что стал админом одним лишним параметром в теле запроса. Один параметр — рут на проде, буквально как в заголовке.

Это не теория, это конвейер CVE

Народ любит говорить «да это баян из методичек». Ага, конечно. Вот что происходит прямо сейчас:

  • GitHub, 2012 — Egor Homakov показал мass assignment в публичных ключах SSH через PR-форму, классика жанра, вошла во все учебники.
  • MISP, CVE-2026-54360 — mass assignment в механизме шаринга объектов, высокая критичность, публикация буквально на днях.
  • Bug bounty в 2026 продолжается бодро: репорты про PUT /api/v1/profile, куда добавляется поле role, и обычный юзер внезапно становится админом — свежий разбор от марта 2026.

Тренд простой: где есть автобиндинг JSON→модель без явного allowlist, там рано или поздно найдётся лишнее поле, которое бэкенд с радостью проглотит.

Векторы атаки: куда совать лишние параметры

Не жди, что уязвимое поле будет очевидным — они прячутся в самых скучных эндпоинтах.

  • Регистрация и профиль: role, isAdmin, is_staff, permissions, account_type, verified, email_verified.
  • Смена пароля / сброс: инъекция параметра id или token в тело запроса, который переопределяет легитимный токен сброса — рабочий вектор для account takeover без взаимодействия с жертвой.
  • E-commerce и биллинг: price, discount, chosen_discount, status — меняешь "chosen_discount": "x" и покупаешь люксовую кожаную куртку за копейки, как в лабе PortSwigger.
  • Bulk-операции: POST /posts/bulkupdate с массивом объектов, где среди легитимных полей затесался user_id или email — меняешь чужой email на свой и потом дергаешь password reset.
  • Multi-tenant SaaS: tenantId, organization_id, plan — привет, доступ к чужому арендатору или бесплатный апгрейд тарифа.

Как искать и эксплуатировать — команды и лайфхаки

Burp Suite тут must-have, не благодари 😎.

  1. Собери baseline-запрос. Перехвати легитимный POST/PUT через Burp Proxy, посмотри JSON-тело.
  2. Найди скрытые поля. Читай доку API, Swagger/OpenAPI-спеку (/swagger.json, /api-docs), исходники фронта (JS-бандлы часто светят названия полей моделей типа isAdmin, role, tier). Это твой словарь для фаззинга.
  3. Автофаззинг через Burp Intruder или Param Miner — закидывай в тело потенциальные имена полей (role, admin, isAdmin, permissions, plan, balance) и смотри на разницу в ответе (статус-код, длина, JSON-структура).
  4. Ручной payload:
PUT /api/v1/profile HTTP/1.1
Host: target.com
Authorization: Bearer <your_token>
Content-Type: application/json

{
  "name": "Bob",
  "email": "bob@test.com",
  "role": "admin"
}

Если в ответе "role": "admin" — вот он, твой рут на проде одним параметром.

  1. Проверь на blind-эффект. Иногда ответ не покажет изменение сразу — перелогинься или дерни другой эндпоинт (/me, /whoami), чтобы убедиться, что привилегия реально закрепилась на бэкенде.
  2. Не забудь про nested-объекты и массивы — уязвимость часто прячется в bulk-эндпоинтах, где валидация ещё слабее, чем в одиночных.

Как закрывать эту дыру (для отчёта клиенту)

Если ты по защите — вот что зашивать в рекомендации:

  • Явный allowlist полей на бэкенде: маппить руками только разрешённые атрибуты, никогда не биндить весь JSON в модель напрямую.
  • Sensitive-поля (role, isAdmin, price, balance) делать read-only относительно клиентского ввода — менять их можно только через отдельные привилегированные эндпоинты с доп. авторизацией.
  • Использовать DTO/Serializer-паттерн вместо прямого биндинга ORM-модели (Django REST serializer.fields, отдельные UpdateUserDTO вместо полной User-модели).
  • Логировать и алертить на неожиданные поля в теле запроса — если клиент прислал поле, которого нет в спеке, это уже сигнал для WAF/IDS.

Короче, бро, Mass Assignment — это баг ленивого биндинга, который живёт ровно столько, сколько разработчики продолжают доверять клиенту больше, чем себе. Один лишний JSON-параметр — и ты уже не юзер, а бог продакшена. Зашивай в чек-лист, тестируй каждый PUT/PATCH на профили и биллинг — там золото копается лопатой.