Защита от массового присваивания

Массовое присваивание возникает в ситуациях, когда входные данные напрямую мапятся на объект модели без контроля допустимых полей. При этом злоумышленник или некорректный клиент может передать дополнительные свойства, которые не предусмотрены бизнес-логикой, но всё равно будут присвоены объекту.

Классический сценарий:

  • сервер принимает JSON-объект
  • объект автоматически преобразуется в экземпляр класса
  • лишние поля попадают в модель
  • происходит несанкционированное изменение состояния

Типичные последствия:

  • изменение ролей пользователя (role: 'admin')
  • модификация служебных флагов (isDeleted, isVerified)
  • подмена идентификаторов владельца (userId, ownerId)
  • обход бизнес-ограничений через скрытые поля

Class-validator сам по себе не выполняет трансформацию данных и не удаляет лишние свойства. Его задача — проверка. Поэтому защита от массового присваивания строится совместно с class-transformer и механизмами фильтрации входных данных.


Роль DTO как границы данных

DTO (Data Transfer Object) выступает в роли явного контракта входных данных. Основной принцип: объект, приходящий извне, не должен напрямую становиться доменной моделью.

import { IsString, IsEmail } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

В таком определении отсутствуют поля вроде role, isAdmin, balance. Это уже формирует базовый уровень защиты, но не гарантирует удаление лишних данных без дополнительной настройки.


Проблема неочищенного объекта

Без фильтрации входных данных объект сохраняет все переданные свойства:

const payload = {
  name: 'Alex',
  email: 'a@mail.com',
  role: 'admin',
  isVerified: true
};

После трансформации без ограничений:

const user = plainToInstance(CreateUserDto, payload);

В объекте могут остаться лишние поля, даже если они не описаны в DTO. Class-validator при этом просто игнорирует неизвестные свойства, не удаляя их.


Включение строгой фильтрации через class-transformer

Основной механизм защиты обеспечивается параметрами трансформации:

whitelist и forbidNonWhitelisted

import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

const dto = plainToInstance(CreateUserDto, payload, {
  whitelist: true,
  forbidNonWhitelisted: true
});

Поведение параметров

whitelist

  • удаляет все свойства, не имеющие декораторов в DTO
  • оставляет только явно описанные поля

forbidNonWhitelisted

  • не просто удаляет лишние поля
  • выбрасывает ошибку при их наличии

Комбинация этих параметров формирует строгий режим обработки входных данных.


Глубокая трансформация входных данных

Дополнительный уровень защиты связан с автоматическим преобразованием plain-object в класс:

{
  transform: true
}

При использовании в связке с валидацией:

const dto = plainToInstance(CreateUserDto, payload, {
  whitelist: true,
  transform: true
});

Это позволяет:

  • привести типы к ожидаемым
  • активировать декораторы class-validator
  • контролировать структуру объекта

Использование ValidationPipe (типовой сценарий)

В архитектурах, где применяется централизованная валидация, используется единая точка контроля:

new ValidationPipe({
  whitelist: true,
  forbidNonWhitelisted: true,
  transform: true
});

Данный набор параметров формирует базовый защитный слой от массового присваивания.


Ограничение поверхностной фильтрации

Whitelist работает только на уровне свойств первого слоя. Для вложенных объектов требуется явное указание трансформации:

import { Type } from 'class-transformer';
import { IsString } from 'class-validator';

class ProfileDto {
  @IsString()
  bio: string;
}

class CreateUserDto {
  @IsString()
  name: string;

  @Type(() => ProfileDto)
  profile: ProfileDto;
}

Без @Type вложенные объекты могут не пройти корректную валидацию и не получить фильтрацию.


Проблема частичных обновлений

При PATCH-запросах возникает конфликт между гибкостью и защитой.

DTO для обновления:

import { IsOptional, IsString } from 'class-validator';

export class UpdateUserDto {
  @IsOptional()
  @IsString()
  name?: string;
}

При использовании whitelist лишние поля всё равно блокируются, даже если они не обязательны. Это предотвращает скрытую модификацию состояния через частичные обновления.


Явное перечисление допустимых полей

Одним из устойчивых подходов является строгая декларация всех разрешённых свойств через DTO без исключений.

Любое поле, отсутствующее в DTO:

  • автоматически удаляется
  • либо вызывает ошибку при строгом режиме

Это делает DTO единственным источником истины для структуры входных данных.


Скрытые поля и механизм исключения

В некоторых случаях требуется наличие полей внутри объекта, но их недопустимо принимать извне.

Используется @Exclude:

import { Exclude } from 'class-transformer';

export class UserDto {
  name: string;

  @Exclude()
  passwordHash: string;
}

Даже при трансформации такие поля не попадают в итоговый объект.


Явное включение полей через @Expose

Альтернативный режим — разрешительный:

import { Expose } from 'class-transformer';

export class UserDto {
  @Expose()
  name: string;

  @Expose()
  email: string;
}

При включённом whitelist и использовании excludeExtraneousValues: true объект будет содержать только отмеченные поля:

plainToInstance(UserDto, payload, {
  excludeExtraneousValues: true
});

Комбинация механизмов защиты

Наиболее строгая конфигурация:

  • whitelist
  • forbidNonWhitelisted
  • transform
  • excludeExtraneousValues
plainToInstance(UserDto, payload, {
  whitelist: true,
  forbidNonWhitelisted: true,
  transform: true,
  excludeExtraneousValues: true
});

Эта комбинация обеспечивает:

  • удаление неизвестных полей
  • запрет на передачу лишних атрибутов
  • контроль явных экспозиций
  • корректную типизацию

Ошибки проектирования DTO

Наиболее частые проблемы, приводящие к обходу защиты:

  • использование одного DTO для create/update/response
  • отсутствие явных декораторов в полях
  • вложенные объекты без @Type
  • использование any или index signatures ([key: string]: any)
  • отсутствие whitelist-режима

Каждая из этих ошибок ослабляет модель данных и делает массовое присваивание возможным.


Разделение моделей входа и выхода

Строгая архитектура предполагает разделение:

  • DTO для входных данных
  • DTO для бизнес-логики
  • DTO для ответа

Входные DTO минимальны и содержат только допустимые поля. Любые вычисляемые или системные поля исключаются из входного контракта.


Ограничение через бизнес-логику

Даже при наличии фильтрации, окончательное предотвращение массового присваивания закрепляется на уровне сервисов:

const user = new UserEntity();
user.name = dto.name;

Прямое присваивание объекта DTO в сущность не используется, что исключает перенос неожиданных полей.


Поведение при обходе фильтрации

Если whitelist отключён, возможны сценарии:

  • сохранение неожиданных полей в базу
  • перезапись защищённых свойств
  • расширение объекта через prototype pollution при некорректной обработке

Поэтому фильтрация входных данных рассматривается как обязательный слой защиты, а не опциональная настройка.