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 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 } If I want to modify the value of 'a', I can write: a.valueOf().x = 3; 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 () 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 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 () a.valueOf(); // { x: 1, y: 2 } 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 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 } 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) 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 } 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,

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
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 }
If I want to modify the value of 'a', I can write:
a.valueOf().x = 3;
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 ()
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
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 ()
a.valueOf();
// { x: 1, y: 2 }
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
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 }
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)
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 }
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);
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.