Prototype pollution — это тип уязвимости, при котором злоумышленник влияет на прототипы встроенных объектов JavaScript через специально сформированные входные данные. В результате изменяются свойства, доступные всем объектам, что приводит к непредсказуемому поведению приложения, обходу проверок и потенциальному выполнению нежелательной логики.
В экосистеме Node.js эта проблема часто возникает при работе с
входящими JSON-объектами, особенно когда они автоматически преобразуются
в экземпляры классов без строгого контроля полей. Библиотека
class-validator используется для декларативной валидации
DTO (Data Transfer Objects), но сама по себе не гарантирует защиту от
prototype pollution без корректной настройки окружения и дополнительных
механизмов фильтрации.
JavaScript позволяет динамически добавлять и изменять свойства объектов. Особую опасность представляют ключи:
__proto__constructorprototypeПри некорректной обработке входных данных эти ключи могут быть интерпретированы как обычные свойства объекта, что приводит к изменению цепочки прототипов.
Пример опасной структуры входных данных:
{
"__proto__": {
"isAdmin": true
}
}
Если такой объект будет без фильтрации объединён с другими объектами
или пройдет через небезопасный парсинг, свойство isAdmin
может появиться у всех объектов приложения.
class-validator работает поверх экземпляров классов и
проверяет значения свойств, используя декораторы:
@IsString()@IsNumber()@IsOptional()@ValidateNested()Однако ключевой момент заключается в том, что библиотека не выполняет автоматическую фильтрацию входных ключей. Она предполагает, что объект уже безопасно сформирован до валидации.
Часто class-validator используется вместе с
class-transformer, который преобразует plain-object в
класс:
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
const dto = plainToInstance(UserDto, request.body);
await validate(dto);
Без дополнительной защиты plainToInstance может
пропустить опасные ключи, если не настроена строгая политика
трансформации.
При преобразовании объекта происходит присваивание свойств:
class UserDto {
name: string;
}
Вход:
{
"name": "Alice",
"__proto__": {
"polluted": true
}
}
Если не включены ограничения, __proto__ может быть
обработан как обычное поле, особенно при небезопасных merge-операциях
или ручном копировании через spread:
const unsafe = { ...request.body };
Этот код не блокирует специальные ключи.
Основной принцип защиты: разрешать только явно описанные поля DTO и отклонять всё остальное.
В экосистеме NestJS это достигается через
ValidationPipe, но аналогичные подходы применимы и без
него.
При включении whitelist лишние поля удаляются:
{
whitelist: true
}
Поведение:
Это снижает риск попадания __proto__ в дальнейшую
обработку.
Более строгий режим:
{
whitelist: true,
forbidNonWhitelisted: true
}
При этом любые дополнительные поля вызывают ошибку валидации, а не просто игнорируются.
Это критично для защиты от prototype pollution, поскольку атака часто опирается на незаметное добавление служебных ключей.
При использовании class-transformer важно ограничивать
преобразование:
plainToInstance(UserDto, payload, {
exposeDefaultValues: false,
enableImplicitConversion: false
});
Дополнительно применяются:
excludeExtraneousValues (при использовании
@Exclude, @Expose)@ExposeПример безопасного DTO:
import { Expose } from 'class-transformer';
import { IsString } from 'class-validator';
export class UserDto {
@Expose()
@IsString()
name: string;
}
Только помеченные поля попадают в объект.
В случаях, когда входные данные поступают из внешних источников без промежуточного слоя валидации, применяется ручная очистка:
const BLOCKED_KEYS = ['__proto__', 'constructor', 'prototype'];
function sanitize(input: Record<string, any>) {
for (const key of Object.keys(input)) {
if (BLOCKED_KEYS.includes(key)) {
delete input[key];
}
}
return input;
}
Более строгий вариант — рекурсивная очистка вложенных объектов.
Частая причина prototype pollution — небезопасное объединение объектов:
const result = {
...defaultConfig,
...userInput
};
Если userInput содержит специальные ключи, они могут
попасть в итоговый объект.
Особенно опасны:
Prototype pollution часто использует вложенные структуры:
{
"a": {
"b": {
"__proto__": { "x": 1 }
}
}
}
Поэтому защита должна включать:
@ValidateNestedПример:
import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';
class ProfileDto {
@IsString()
bio: string;
}
class UserDto {
@ValidateNested()
@Type(() => ProfileDto)
profile: ProfileDto;
}
class-validator:
Это означает, что ответственность за фильтрацию лежит на этапе трансформации и конфигурации pipeline.
На уровне архитектуры применяются следующие принципы:
Каждый входящий объект должен иметь строго определённую структуру без динамических ключей.
Избегается использование:
Object.assign с пользовательскими даннымиreq.bodyВсе входные данные проходят через единый слой обработки, где:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true
})
);
Эта конфигурация обеспечивает:
Prototype pollution редко остаётся изолированной проблемой. Она становится базой для:
В связке с динамическим JavaScript это приводит к трудно обнаружимым дефектам, поскольку изменения происходят на уровне прототипа, а не конкретного объекта.
Безопасная цепочка обработки включает:
class-validatorКлючевой принцип — ни один пользовательский объект не должен попадать в логику без прохождения всех этапов фильтрации.
class-validator решает задачу проверки значений, но не
занимается безопасностью структуры объекта на уровне прототипов. Защита
от prototype pollution требует комбинирования нескольких слоёв:
Только совокупность этих механизмов формирует устойчивую защиту от изменения прототипов через пользовательский ввод.