DEV Community

naugtur
naugtur

Posted on

How private are your class #private fields?

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)'));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' ]
Enter fullscreen mode Exit fullscreen mode

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)