@@toStringTag spoofing for null and undefined

Allen Wirfs-Brock allen at wirfs-brock.com
Tue Jan 20 15:51:13 PST 2015

On Jan 20, 2015, at 11:38 AM, Jordan Harband wrote:

> "X is bad" is, quite frankly, mostly irrelevant here. When X is bad in JS, then TC39 seems to have gone with primarily one of two choices: use strict mode to remove it, or, provide a better option Y so that developers *want* to move away from X and start using Y. In this case, the value of type checking doesn't matter - people do it, and so it must be considered.
> There are two primary usages of Object.prototype.toString in my eyes:
>  - constructs like `Object.prototype.toString.call(new String('foo')) === Object.prototype.toString.call('foo')` (or using that to throw an explicit error "don't use object forms of primitives") which ensures that people using my code won't fall into the very common footgun (thanks, Java) of using boxed primitives. (The same issue will apply to Symbols in ES6, but at least I can do `typeof Symbol.prototype.toString.call(value) === 'symbol'`)
>  - Testing of values from other realms (namely iframes). `Object.prototype.toString.call(value) === '[object Array]'` is a remarkably consistently written return value of an `Array.isArray` polyfill, since it works no matter how somebody has monkeyed with any global Array object.
> Certainly it is trivial to construct a malicious array, for example, and pass that around breaking all sorts of things. The goal, in my opinion, of Object.prototype.toString checking is *not* security - it's avoiding common developer hazards. In other words, I want my code to fail fast when the developer unintentionally passes me the wrong thing - something that I think we can agree happens often.
> 1) configurability of @@toStringTag
> It appears that TC39 considers it important to not break existing JS code with spec changes. Thus, `Object.prototype.toString.call(foo)`, for any ES5 value "foo", must always and forever return the same value that it returned in ES5 - otherwise, existing code may follow different code paths in ES5 versus ES6, which is a hazard. This leads me to the belief that @@toStringTag values on ES5 builtins should never be changeable.
> Some have replied to this, "if you don't run first, all bets are off - freeze it if you want it". Fair point! If you don't run first and keep a reference to Object.prototype.toString, you're screwed anyways. However, in ES5, if I *do* run first, I have 100% opt-in protection against somebody breaking things I care about. Essentially all polyfilled `Array.isArray` code on the web could break if it is possible to redefine `Array.prototype[@@toStringTag]` (in any realm, not just the one I start in), which is currently possible by default in ES6.
> I believe that all built-in objects, in all realms, should have a nonconfigurable @@toStringTag for this reason.
> 2) Since people *are* doing this type checking, if I make an object that defines its @@toStringTag value to return "Array", I will break code that does this. (Hence the protections in https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.prototype.tostring) I've filed https://bugs.ecmascript.org/show_bug.cgi?id=3506 to discuss the missing values (namely Math, JSON, and Object, and Null, and Undefined have been mentioned as well).
> Currently, the spec handles this by including a whitelist of values, and specifying that any value not in this list receives a "~" prefix. (The value of the prefix itself is irrelevant, let's please not bikeshed it).
> My proposal is that rather than maintain a whitelist, and have that added complexity, that ES6 specifies that any user-defined @@toStringTag value will *always* and unconditionally have a prefix applied.
> --------------------
> I believe that either of these proposals by themselves will be a win - but with both together, "nominal type checking" / branding code will continue to work, and there will be no hazards or footguns by default.
> For those who dislike this kind of code, I challenge you and the committee to finalize and publish a better approach for answering the question "does this behave like an array" or "does this behave like a map" that work cross-realm (besides exhaustive duck-typing and/or feature detection), rather than attempting to simply oppress what many consider to be a valid approach, and a functional and existing one.

there are two things going on with @@toStringTag and  ES6 O.p.toString

1)  @@toStringTag is primarily intended as an oopen-ended extension point that allows JS class (and other abstraction)  to parameterize the descriptive string produced by O.p.toString.  This parameterization is desirable (as an alternative or supplement to over-riding toString) in support of (usually debugging) use cases where O.p.toString called as a reliable means to get a displayable "[  ]" description of an object. 

For that purpose, we don't really care whether an object's @@toStringTag is stable or even whether it spoofs one of the legacy tag values. It's just a hook for plugging into that extension point for that purpose.

2) [[Class]] is gone because it did not provide an extensible way to do brand checking yet it was (indirectly through O.p.toString) being used in that way. O.p.toString never provided a way to do brand checking on values defined using JS definable constructors. When and if we decide that we can no longer live without such an extensible branding mechanism we will define one, with its one mechanism and interface.  We wouldn't misuse O.p.toString for that purpose. 

If developers started to use @@toStringTag as a brand that they access using O.p.toString (and we start making accommodations in support of that usage) we'd be worse off WRT branding then we are with ES5. 

I would sooner see @@toStringTag go completely way than see it start to be used as a brand.  If that is how we really think it will go down, then I say we should screw the convenience that we were trying to offer with #1 above  and completely revert to what is essential ES5 behavior.  In therms of the current spec. that means anything allocated by a ES defined constructor would answer "[object Object]".

As I said in a tweet yesterday: Our dilemma: some things that made sense in the past & people are used to, don't make future sense

Using, O.p.toString is one of those things.  If improving the "produce a debug string" behavior is going to reenforce a O.p.toString usage that doesn't make sense for the future (or even for ES6), then we probably shouldn't improve O.p.toString.

(But one remaining problem, if we lost @@toStringTag then there isn't any particularly good way to explain the behavior of it when applied to DOM objects.)


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20150120/1536b044/attachment.html>

More information about the es-discuss mailing list