@@toStringTag spoofing for null and undefined

Domenic Denicola d at domenic.me
Wed Jan 21 06:19:00 PST 2015

I’d urge us to be more cautious about throwing away, or postponing, the solid win that is @@toStringTag. This is something we’ve wanted for a long time.

I’ve recently gotten several requests for jsdom to support DOM-compatible return values for Object.prototype.toString [1] [2], and each time I’ve told the requestors that we’re going to have to wait for proper @@toStringTag support [3] [4] to get a solid emulation. Given that @@toStringTag looks to be on-track to ship within one or two more V8 releases, I was very excited to be able give those users what they’re asking for finally.

Even more so, on the W3C TAG we’ve recently been working on an “Extensible Web Report Card” [5], regarding how well the platform can be explained. One of our best success stories is “Explaining the DOM via JavaScript” [6], which talks about how with the advent of ES6 (proxies, weak maps, and more) we can finally self-host all of the DOM (with the exception of `document.all`, of course). It would be a shame to move from virtually-100% self-hostable to 0% in one decision!


As I’ve tried to emphasize, although I can understand where Jordan’s concern comes from, I don’t think it’s a compelling one. He even states:

> The goal, in my opinion, of Object.prototype.toString checking is not security - it's avoiding common developer hazards.

Given this, I do not think we should be worried at all about the consequences of @@toStringTag. The scenario he envisions---where someone overrides Array.prototype[Symbol.toStringTag]---is far outside the realm of "common developer hazards." Saying that code intended for

>  "is this likely to behave like an array" without ducktyping or feature testing

is not resilient in the face of people modifying the built-in prototypes seems *absolutely fine*. As Allen has pointed out, the test is not reliable at all: even if you freeze `Array.prototype[Symbol.toStringTag]`, I can override `Array.prototype` and `Array.prototype.__proto__` to cause "is this likely to behave like an array" to be false no matter what `Object.prototype.toString` returns. (If you plan on saving away the values of those and using them on array literals only, then I'm just going to install a non-configurable throwing setter on `Array.prototype[0]`, and you still won't be able to use them effectively. This is not a winnable game!) In Jordan's message, he states that he's not concerned about these kinds of scenarios. Instead,
> I'm concerned about people passing me things and *not realizing* that it's not what I wanted them to pass

But in this case you should *definitely* not be concerned about code that has overwritten `Array.prototype[Symbol.toStringTag]`, as it's so far outside the realm of expected API usage that it's clearly purposeful!


We've been through this "should we lock it down" dance a couple times just in my time on the committee. The most prominent analogy is in my mind Function.prototype.length, which we specifically *un*-locked to be user-configurable---in the same way as @@toStringTag, by making it non-writable but configurable.

And I think in general this is a good decision. For every built-in property or method, you can invent a use case of similar plausibility to "is this likely to behave like an array" that would advise toward locking down that property or method. For example, people often serialize out functions with Function.prototype.toString and parse/manipulate them---and we don't want that to be broken if someone modifies Function.prototype.toString "without realizing" the consequences, so we should freeze Function.prototype.toString! But the fallacy here is believing that we need these use cases to be resilient in the face of modified built-ins.

I understand the fact that un-locking toString-tags is a new ability in ES6 that ES5 didn't have. But we have lots of these, from Function.prototype.length onward [7]. (Another good analogy is the various changes from magic own-data properties to configurable getters---now people can override those getters and break various expectations.) Nothing of value is lost, because if a collection of code messes with built-in prototypes, all of your attempted invariants (like, `Object.prototype.toString.call(a) === "[object Array]"` => `Array.prototype.forEach` will work on the object) are up in the air anyway. Much more often we're going to have people modifying them who *know what they're doing*, and are prepared to reap benefits from them.

[1]: https://github.com/tmpvar/jsdom/issues/766
[2]: https://github.com/tmpvar/jsdom/issues/935
[3]: https://github.com/tmpvar/jsdom/pull/945#issuecomment-62369868
[4]: https://github.com/tmpvar/jsdom/pull/970#issuecomment-65869082
[5]: http://extensiblewebreportcard.org/
[6]: https://extensiblewebreportcard.org/#toc-7
[7]: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-in-the-6th-edition

More information about the es-discuss mailing list