Массовое присваивание возникает в ситуациях, когда входные данные напрямую мапятся на объект модели без контроля допустимых полей. При этом злоумышленник или некорректный клиент может передать дополнительные свойства, которые не предусмотрены бизнес-логикой, но всё равно будут присвоены объекту.
Классический сценарий:
Типичные последствия:
role: 'admin')isDeleted,
isVerified)userId,
ownerId)Class-validator сам по себе не выполняет трансформацию данных и не удаляет лишние свойства. Его задача — проверка. Поэтому защита от массового присваивания строится совместно с class-transformer и механизмами фильтрации входных данных.
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 при этом просто игнорирует неизвестные свойства, не удаляя их.
Основной механизм защиты обеспечивается параметрами трансформации:
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
const dto = plainToInstance(CreateUserDto, payload, {
whitelist: true,
forbidNonWhitelisted: true
});
whitelist
forbidNonWhitelisted
Комбинация этих параметров формирует строгий режим обработки входных данных.
Дополнительный уровень защиты связан с автоматическим преобразованием plain-object в класс:
{
transform: true
}
При использовании в связке с валидацией:
const dto = plainToInstance(CreateUserDto, payload, {
whitelist: true,
transform: true
});
Это позволяет:
В архитектурах, где применяется централизованная валидация, используется единая точка контроля:
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;
}
Даже при трансформации такие поля не попадают в итоговый объект.
Альтернативный режим — разрешительный:
import { Expose } from 'class-transformer';
export class UserDto {
@Expose()
name: string;
@Expose()
email: string;
}
При включённом whitelist и использовании
excludeExtraneousValues: true объект будет содержать только
отмеченные поля:
plainToInstance(UserDto, payload, {
excludeExtraneousValues: true
});
Наиболее строгая конфигурация:
plainToInstance(UserDto, payload, {
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
excludeExtraneousValues: true
});
Эта комбинация обеспечивает:
Наиболее частые проблемы, приводящие к обходу защиты:
@Typeany или index signatures
([key: string]: any)Каждая из этих ошибок ослабляет модель данных и делает массовое присваивание возможным.
Строгая архитектура предполагает разделение:
Входные DTO минимальны и содержат только допустимые поля. Любые вычисляемые или системные поля исключаются из входного контракта.
Даже при наличии фильтрации, окончательное предотвращение массового присваивания закрепляется на уровне сервисов:
const user = new UserEntity();
user.name = dto.name;
Прямое присваивание объекта DTO в сущность не используется, что исключает перенос неожиданных полей.
Если whitelist отключён, возможны сценарии:
Поэтому фильтрация входных данных рассматривается как обязательный слой защиты, а не опциональная настройка.