Making operations on property descriptors more robust against Object.prototype hazards

# Claude Pache (11 years ago)

As noted some time ago, there is an issue with Object.defineProperty when it happens that someone has the facetious idea to give some value to Object.prototype.get [1].

While it is hardly an issue in day-to-day programming, I think that it is a good idea to make sure that specialized API (Reflect, proxy traps) are robust by design against that bug^H^H^H feature. Concretely, I propose:

(1) proxy.[[DefineOwnProperty]] — When a proxy defines the corresponding trap, it shall receive an object with no prototype as property descriptor.

(2) Reflect.getOwnPropertyDescriptor —That method shall return an object with no prototype, so that it can be readily used elsewhere, e.g. in Reflect.defineProperty.

—Claude

[1] esdiscuss.org/topic/object

# Claude Pache (11 years ago)

Le 13 sept. 2014 à 11:47, Claude Pache <claude.pache at gmail.com> a écrit :

Hi,

As noted some time ago, there is an issue with Object.defineProperty when it happens that someone has the facetious idea to give some value to Object.prototype.get [1].

While it is hardly an issue in day-to-day programming, I think that it is a good idea to make sure that specialized API (Reflect, proxy traps) are robust by design against that bug^H^H^H feature. Concretely, I propose:

(1) proxy.[[DefineOwnProperty]] — When a proxy defines the corresponding trap, it shall receive an object with no prototype as property descriptor.

(2) Reflect.getOwnPropertyDescriptor —That method shall return an object with no prototype, so that it can be readily used elsewhere, e.g. in Reflect.defineProperty.

—Claude

[1] esdiscuss.org/topic/object-prototype-get-bye-bye-object-defineproperty

One can also (either alternatively or in addition) correct the other end of the API: (1) in proxy.[[GetOwnProperty]], ignore non-own properties of the result of the trap, and (2) in Reflect.defineProperty, ignore non-own properties of the object descriptor.

The core problem is that the two internal operations that convert between an internal representation of a property descriptor on the one side, and a concrete object representing a property descriptor on the other side, are not the inverse one of the other, because of the interference of Object.prototype.

# Claude Pache (11 years ago)

Le 13 sept. 2014 à 14:19, Claude Pache <claude.pache at gmail.com> a écrit :

Le 13 sept. 2014 à 11:47, Claude Pache <claude.pache at gmail.com> a écrit :

Hi,

As noted some time ago, there is an issue with Object.defineProperty when it happens that someone has the facetious idea to give some value to Object.prototype.get [1].

While it is hardly an issue in day-to-day programming, I think that it is a good idea to make sure that specialized API (Reflect, proxy traps) are robust by design against that bug^H^H^H feature. Concretely, I propose:

(1) proxy.[[DefineOwnProperty]] — When a proxy defines the corresponding trap, it shall receive an object with no prototype as property descriptor.

(2) Reflect.getOwnPropertyDescriptor —That method shall return an object with no prototype, so that it can be readily used elsewhere, e.g. in Reflect.defineProperty.

—Claude

[1] esdiscuss.org/topic/object-prototype-get-bye-bye-object-defineproperty

One can also (either alternatively or in addition) correct the other end of the API: (1) in proxy.[[GetOwnProperty]], ignore non-own properties of the result of the trap, and (2) in Reflect.defineProperty, ignore non-own properties of the object descriptor.

The core problem is that the two internal operations that convert between an internal representation of a property descriptor on the one side, and a concrete object representing a property descriptor on the other side, are not the inverse one of the other, because of the interference of Object.prototype.

—Claude

Sorry for the spam, but I thought that a concrete example is good to illustrate the issue. Currently, under the assumption that someone will write Object.prototype.get = function() {}, the following proxy:

Proxy(target, {
    defineProperty: (target, prop, desc) => Reflect.defineProperty(target, prop, desc)
  , getOwnPropertyDescriptor: (target, prop) => Reflect.getOwnPropertyDescriptor(target, prop)
})

is broken, and the following code:

desc = Reflect.getOwnPropertyDescriptor(source, prop)
if (desc)
    status = Reflect.defineProperty(target, prop, desc)

is broken as well.

# Brendan Eich (11 years ago)

It's not nice to lose .toString, though.

Tom, didn't you have an idea for a fix? I can't find it but vaguely recall it (or dreamt it!).

# Tom Van Cutsem (11 years ago)

2014-09-13 17:49 GMT+02:00 Brendan Eich <brendan at mozilla.org>:

It's not nice to lose .toString, though.

Tom, didn't you have an idea for a fix? I can't find it but vaguely recall it (or dreamt it!).

No, not that I know of. As highlighted in the old thread linked to by Claude, there is a legitimate use for considering inherited attributes: inheriting defaults. Brandon Benvie stepped in to acknowledge he actually used this pattern.

Of course, in the old thread we were talking about Object.{dP, gOPD} which we can't change because of backwards-compat issues. Claude is suggesting we can still change Reflect.{dP, gOPD} in this regard. I'm hesitant to do so though, because I feel the root cause of the problem is the monkey-patching of Object.prototype, rather than property descriptors being "badly designed" just because they consider inherited attributes. Even if we would fix this particular part of the Reflect API, code that does Object.prototype.get = ... is likely to cause other bugs as well.

Speaking about that particular example: if one adds Object.prototype.get = function(){}, then trying to define a data descriptor will immediately fail noisily because the spec explicitly checks that a descriptor can't be both a data and accessor property at the same time, so this particular conflict is likely to be detected very soon.

# Mark S. Miller (11 years ago)

+1

Adding string-named properties to Object.prototype will create all sorts of hazards. The only way to avoid such hazards is not to do that. We do not need to pervert other APIs to make this fatally bad practice slightly less fatal.

If you want to actually defend against such hazards rather than blindly trusting all you code not to add properties to Object.prototype, do

Object.preventExtensions(Object.prototype);

However, this will also prevent the addition of symbol-named properties, which are still problematic but much less so.

(Or you could load SES and not have to worry about any such problems ;).)

# Andrea Giammarchi (11 years ago)

IIRC Allen proposed to change specs so that inheritance was considered down to Object.prototype but without including it.

A snippet that some how describes the logic would be something like the following

function defineProperty(obj, prop, descriptor) {
  var
    withOwnDesc = {
      __proto__: null,
      enumerable: findProperty(descriptor, 'enumerable'),
      configurable: findProperty(descriptor, 'configurable')
    },
    get = findProperty(descriptor, 'get'),
    set = findProperty(descriptor, 'set')
  ;
  if (!get && !set) {
    withOwnDesc.writable = findProperty(descriptor, 'writable');
    withOwnDesc.value = findProperty(descriptor, 'value');
  } else {
    if (get) {
      withOwnDesc.get = get;
    }
    if (set) {
      withOwnDesc.set = set;
    }
  }
  return Object.defineProperty(obj, prop, withOwnDesc);
}

function findProperty(obj, prop) {
  var has = Object.prototype.hasOwnProperty;
  while (!has.call(obj, prop)) {
    obj = Object.getPrototypeOf(obj);
    if (!obj || obj === Object.prototype) {
      return false;
    }
  }
  return obj[prop];
}

Object.prototype.configurable = true;
var p = {enumerable: true};
var o = Object.create(p);
o.writable = true;
o.value = 123;


defineProperty({}, 'test', o);

// descriptor would be
{
  writable: true,
  enumerable: true,
  configurable: false,
  value: 123
}

Moved into native might not be such huge problem, performance speaking, but the web survived until now so maybe it's OK to keep it fragile as it is, it has also the benefit of making suddenly everything enumerable and configurable for testing purposes so ...

Best

# Erik Arvidsson (11 years ago)

Adding a new way to interpret properties is a bad idea. No engine is going to optimize this new special form of [[Get]] and this will deopt all calls that directly depend on descriptors.

# Andrea Giammarchi (11 years ago)

I have simply shown how the logic would most likely be implemented via JS, not how C++ engine should do that so performance on JS side are not "the" concern but like I've said, the whole thing is probably not worth a fix.

However, I do believe nobody in the real world use inheritance for descriptors ... so I'd rather simply drop this potentially dangerous behavior and ask Brandon Benvie to simply returns {properties} in his constructors used as descriptors.

I find that a complete anti pattern ... how do you even call them, EnumerableButNotConfigurable, EnumerableAndConfigurable ... what the hack ?

Just my opinion, it should not have been considered inherited properties since the beginning.

Best

# Claude Pache (11 years ago)

Le 14 sept. 2014 à 01:58, Mark S. Miller <erights at google.com> a écrit :

+1

Adding string-named properties to Object.prototype will create all sorts of hazards. The only way to avoid such hazards is not to do that. We do not need to pervert other APIs to make this fatally bad practice slightly less fatal.

If you want to actually defend against such hazards rather than blindly trusting all you code not to add properties to Object.prototype, do

Object.preventExtensions(Object.prototype);

However, this will also prevent the addition of symbol-named properties, which are still problematic but much less so.

Yes, and it would be nice to have more fine-grained methods than Object.preventExtensions. For example:

Object.forbidProperties(Object.prototype, ['value', 'writable', 'get', 'set', 'configurable', 'enumerable'])

Object.forbidNumericalProperties(Array.prototype)

This could be experimented with proxies... although it will be easy to circumvent the proxy by using Object.getPrototypeOf({}) instead of Object.prototype, unless we monkey-patch Object.getPrototypeOf, Object.prototype.__proto__, Reflect.getPrototypeOf...

―Claude

# Andrea Giammarchi (11 years ago)

not a bad idea, here something to play with:

Object.forbidProperties = (function(descriptor){
  function makeForbidden(property) {
    Object.defineProperty(this, property, descriptor);
  }
  descriptor.get = function () {
    return void 0;
  };
  descriptor.set = function (value) {
    throw new Error('unable to set: ' + value);
  };
  descriptor.enumerable = false;
  descriptor.configurable = false;
  return function forbidProperties(obj, properties) {
    properties.forEach(makeForbidden, obj);
    return obj;
  };
}(Object.create(null)));

makes this possible:

Object.forbidProperties(Object.prototype, ['value', 'writable', 'get',
'set', 'configurable', 'enumerable']);

although all those will show up through getOwnPropertyNames ... slightly bummer