DEV Community

Cover image for Variables in Closures Are Not as Secure as You Think
MingYuan ⌨
MingYuan ⌨

Posted on

Variables in Closures Are Not as Secure as You Think

I've always thought that closures were a way to implement private variables, but today I discovered that variables within a closure can be modified, which took me by surprise!

let obj = (function () {
    let a = {
        x: 1,
        y: 2,
    };
    return {
        getKey(key) {
            return a[key];
        },
    };
})();

console.log(obj.getKey('x'));
// 1
Enter fullscreen mode Exit fullscreen mode

Here, 'a' is a variable within the closure, and I only exposed the getKey method. However, I can still access and modify the value of 'a'.

If I want to modify the value of 'a', I definitely can't follow the usual approach.

After some careful thought, I considered the prototype chain. Objects have a valueOf method on their prototype chain, which returns the object itself.

let a = {
    x: 1,
    y: 2,
};

console.log(a.valueOf());
// { x: 1, y: 2 }
Enter fullscreen mode Exit fullscreen mode

If I want to modify the value of 'a', I can write:

a.valueOf().x = 3;
Enter fullscreen mode Exit fullscreen mode

So, we can use this to try to modify the value of 'a':

console.log(obj.getKey('valueOf')());
// Uncaught TypeError: Cannot convert undefined or null to object
//     at valueOf (<anonymous>)
Enter fullscreen mode Exit fullscreen mode

Calling it this way doesn't directly get the value of 'a'; instead, it throws an error. Do you know why?

To cut to the chase, it's a this context issue. this points to window, not the 'a' object.

Why does this point to window? Because the valueOf method is called by window, not the 'a' object.

Here's a simple example:

const f = function () {
    console.log(this);
};

f();
// window

const a = {
    f,
};

a.f();
// a
Enter fullscreen mode Exit fullscreen mode

If f is called directly, this points to window. If a.f() is called, this points to 'a'. So, the this context can basically be understood as: who calls the function, this points to them.

Back to our situation, the value of obj.getKey('valueOf') is the valueOf function itself. Then, we directly call the valueOf function, so this points to window, not the 'a' object, because it's not the 'a' object calling the function; it's being called directly.

To be more specific:

const a = {
    x: 1,
    y: 2,
};

const valueOf = a.valueOf;

valueOf();
// Uncaught TypeError: Cannot convert undefined or null to object
//     at valueOf (<anonymous>)

a.valueOf();
// { x: 1, y: 2 }
Enter fullscreen mode Exit fullscreen mode

Calling the valueOf function directly globally throws an error. Calling it using the 'a' object returns correctly.

You might say, can't I just change the this context? Well, you actually can't. Here's an example:

const a = {
    x: 1,
    y: 2,
};

function f() {
    console.log(this);
}

f.call(a);
// a
Enter fullscreen mode Exit fullscreen mode

When we call the call, bind, or apply methods, we change the this context, but we need a parameter, which is the 'a' object. Our problem is that we can't get the 'a' object.

Then why are you talking so much if you can't do it?

Don't worry, here's the main point:

We can add a method to Object.prototype and then call it:

Object.prototype.showYourSelf = function () {
    return this;
};

const a = {
    x: 1,
    y: 2,
};

a.showYourSelf();
// { x: 1, y: 2 }
Enter fullscreen mode Exit fullscreen mode

This way, we can get the 'a' object. Let's try some code:

Object.prototype.showYourSelf = function () {
    return this;
};

console.log(obj.getKey('showYourSelf')());
// window (undefined in strict mode)
Enter fullscreen mode Exit fullscreen mode

We find that it's still wrong. We haven't gotten the 'a' object. You probably know the reason: it's still a this context issue. Because showYourSelf is called globally, this points to the global object, not the 'a' object.

So, what else can we do? Is this this context problem unsolvable?

No, I won't keep you in suspense. Here's the complete code:

'use strict';
let obj = (function () {
    let a = {
        x: 1,
        y: 2,
    };
    return {
        getKey(key) {
            return a[key];
        },
    };
})();

Object.defineProperty(Object.prototype, 'showYourSelf', {
    get() {
        return this;
    },
});

console.log(obj.getKey('showYourSelf'));
// { x: 1, y: 2 }

obj.getKey('showYourSelf').z = 3;
console.log(obj.getKey('showYourSelf'));
// { x: 1, y: 2, z: 3 }
Enter fullscreen mode Exit fullscreen mode

Do you know about the "getter" technique? A getter is a function that is called when an object property is accessed, and the caller is the object itself. So, this in the getter function points to the object itself. Since we can get the 'a' object itself from the closure, modifying it is easy.

You might say, this method is impressive, but what's the use? It's just your self-indulgence.

No, for example, if you're a library author and you store a URL that uploads user tokens in a closure variable, you might mistakenly trust that the closure variable won't change. But a hacker might write code like this to change your closure variable, causing user tokens to be uploaded to the hacker's website. If your code is running on a Node.js server, the consequences could be dire.

Now that we know the attack principle, the defense is easy: prevent closure variables from traversing the prototype chain:

let a = Object.create(null);
a.x = 1;
a.y = 2;

// Or
let a = {
    x: 1,
    y: 2,
};

Object.setPrototypeOf(a, null);
Enter fullscreen mode Exit fullscreen mode

Both of these methods set the prototype of the 'a' object to null, so the 'a' object won't traverse the prototype chain and won't be attacked.

Finally, we should understand code and knowledge at a deeper level, from the principles, not just superficially. This way, we can calmly deal with these strange problems.

For example, the 'a' object we create directly is not clean; it's not a pure object. It has a prototype chain, which is why a.valueOf() can be called correctly. Because the prototype chain of the 'a' object has the valueOf method. If it were a clean, pure object, a.valueOf() would throw an error because the 'a' object wouldn't have the valueOf method.

Also, for example, the property 'x' of the 'a' object has property descriptors (such as whether the property can be iterated by for...in), and it can also have getters and setters.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

DEV shines when you're signed in, unlocking a customized experience with features like dark mode!

Okay