The word "private" is defined as:
belonging to or for the use of one particular person or group of people only.
Let's figure out who those people are.
EcmaScript Class
class MyClass {
#privateField;
constructor(value) {
this.#privateField = value;
}
getPrivate(otherInstance) {
return otherInstance.#privateField;
}
}
const a = new MyClass("AA");
const b = new MyClass("BB");
console.log(a.getPrivate(b)); // BB
In native classes the private reference belongs to the class and every instance of that class can access other instances' private field values.
Ok, so this is fine. All I need to do to avoid giving away my private fields is to not have a method that uses them from a reference given to me in input.
Well, yes, but this
reference is also input to functions in JavaScript
class MyClass {
#privateField;
constructor(value) {
this.#privateField = value;
}
getMyPrivate() {
return this.#privateField;
}
}
const a = new MyClass("AA");
const b = new MyClass("BB");
console.log(a.getMyPrivate()); // AA
console.log(b.getMyPrivate.bind(a)()); // AA
Would type-checking help?
Let's take a look at a similar example in TypeScript.
Our adversary is trying to read private fields from our classes at runtime. All type safety is resolved at compile time though.
Let me remind you what a wise man once said:
TypeScript is a seatbelt you wear while the car is stopped and unbuckle when you start the engine.
Let's look at this typescript class:
class TsClass {
#privateField;
constructor(value) {
this.#privateField = value;
}
getPrivate() {
return this.#privateField;
}
}
const a = new TsClass('AA');
const b = new TsClass('BB');
console.log(a.getPrivate()); // AA
console.log(eval('_TsClass_privateField.get(b)')) // BB
It doesn't fail to compile because the adversarial code is in a string, but this just emulates an XSS or supply chain attack.
Why does it work? Well, this is the result of running the sample through tsc
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _TsClass_privateField;
class TsClass {
constructor(value) {
// Private field
_TsClass_privateField.set(this, void 0);
__classPrivateFieldSet(this, _TsClass_privateField, value, "f");
}
getPrivate() {
return __classPrivateFieldGet(this, _TsClass_privateField, "f");
}
}
_TsClass_privateField = new WeakMap();
const a = new TsClass('AA');
const b = new TsClass('BB');
console.log(a.getPrivate());
console.log(eval('_TsClass_privateField.get(b)'));
It's all represented with a conventionally named WeakMap.
You can target es2022 or above to get TypeScript to generate the same code as in the EcmaScript example.
Is anything private?
Yes. It's the good old closures. Functional scope is the real private thing in JavaScript.
function NotAClass(value1, value2) {
const actuallyPrivate = [value1, value2];
return {
doSomethingWithPrivate: () => actuallyPrivate.join('-'),
};
}
const c = NotAClass("CC", "DD");
console.log(c.doSomethingWithPrivate()); // CC-DD
It's really private.
Until you consider the possibility of prototype poisoning
Array.prototype.join = function () {
console.log('join',this);
}
c.doSomethingWithPrivate()
// logs: join [ 'CC', 'DD' ]
But that's a whole different story...
If you're interested, check out my defensive-coding training or look into tools that can prevent prototype poisoning and much more:
Top comments (0)