Can Function.prototype.toString.call() be spoofed?

I'm looking at Lodash's implementation for _.isPlainObject() which does a number of different checks in there to try to determine in the passed-in value is an instance of Object in a cross-realm compatible way. One of these checks is effectively the following:

Function.prototype.toString.call(value) === Function.prototype.toString.call(Object)
// Function.prototype.toString.call(Object) is, as far as I can tell, always equal to
// 'function Object() { [native code] }', though perhaps it's possible for platforms to return something else here?
// Maybe some older platforms used to return other stuff, but now that's not allowed anymore?

Questions:

  1. Is it true that Function.prototype.toString.call(Object) will always return function Object() { [native code] }? ! Maybe older specifications didn't require this behavior and Lodash was trying to account for that?

  2. Is it possible for this check to ever get spoofed?

For question 1, my reading of the spec for Function.prototype.toString makes it feel like this would always return the string function Object() { [native code] } - it talks about how initialName is stored in an internal slot, and that's what's used to determine the name Object that gets put in the resulting string. Just wanting to verify this assumption.

For question 2, Lodash does a number of checks in addition to the one shown above, which makes me feel like the author knew the check could be spoofed, so they were trying to layer a number of additional checks onto it to reduce the likelyhood of this happening. But, I can't think of any way to spoof it, aside from working in exotic platforms that maybe decides to oddly provide a second built-in Object function that happens to stringify to the exact same string, or something like that.

Nevermind, derp on my part.

It's not that Function.prototype.toString() can be spoofed, it's that the value being passed in is basically Object.getPrototypeOf(valueYouPassedIn).constructor, and that constructor field could be spoofed to be anything. So, it looks like I can fool Lodash's _.isPlainObject() algorithm like this:

class SpecialClass {
  static {
    SpecialClass.prototype.constructor = Object;
  }
}

_.isPlainObject(new SpecialClass()); // true

I'm still not entirely sure why it was trying to do additional checks on the value's prototype's constructor (e.g. it also checked if it was an instance of itself) - I feel like the Function.prototype.toString.call() check should have been sufficient there, but this at least explains to me why it feels like there was a myriad of different checks going on.

I think the string format of Object, spec-wise, is fairly new, part of Function.prototype.toString revision in ES2019. Before that it was an "implementation-dependent String source code representation" but I'm assuming all the major players followed that same format anyway if it eventually ended up in the spec that way.

Not having that guaranteed is maybe where the instanceof check came in since it will see if the constructor was also in the prototype chain of a function, which only applies (normally) to Function and Object.

But I'm also wondering why there's not an additional check for the prototype chain as well since a proto === Ctor.prototype would be all that would be needed to prevent your spoof from working. Unless they purposefully want objects like the following to be considered plain:

const obj = {
    x : 1,
    __proto__: { y: 2 }
}

... though that would fail the hasOwnProperty.call(proto, 'constructor') check, so maybe not?

1 Like
proto === Ctor.prototype

I don't think that would work in a cross-realm-compatible way - e.g. I receive a plain object from an iframe and compare it to the Object constructor's prototype from outside the iframe and they would be different.

Yeah in my opinion a plain object is when proto === null or proto === Object.prototype. i don't think there is any reliable way to differentiate a cross realm Object proto from a same realm custom class.

Object.prototype has an immutable [[Prototype]] without being frozen (as does window i believe), so that might be one heuristic you could use.

It's not comparing with the current realm's Object's prototype; its comparing the constructor pulled out of the provided object. So unless that object is a frankenstein's monster assembled from parts from different realms, these should each be from the same realm.

The problem is that you trust the target .constructor property in the first place. That can return roughly anything, and you can't differentiate it from an intrinsic Object constructor from another realm.

Perhaps I could also explain at a higher-level what I'm trying to do.

I'm trying to make a catalog of every Lodash function, and how one might go about doing the same kind of thing without Lodash (and, boy, there's a lot of Lodash functions, it's a little tedious but I'm about half way through). I'm not trying to provide alternatives that have the exact same implementation as the Lodash's functions, it's more like "I'd normally reach for _.x() because of Y - how might I instead do Y without Lodash?" Sometimes the alternative is dead-simple, sometimes it's a fair bit more involved. Right now I'm working on the various _.isWhatever() functions.

So, for this _.isPlainObject() function, I'm thinking that a good "vanilla" JS alternative could be something like this:

function isPlainObject(value) {
  if (value === null || value === undefined) {
    return false;
  }

  const protoOf = Object.getPrototypeOf;
  if (protoOf(value) === null) {
    return true;
  }

  const objectConstructorAsString = 'function Object() { [native code] }';
  return (
    protoOf(protoOf(value)) === null &&
    typeof protoOf(value).constructor === 'function' &&
    Function.prototype.toString.call(protoOf(value).constructor) === objectConstructorAsString
  );
}

This implementation can be fooled (but as @mhofman mentioned, there's really not a way to get around that today):

// Fooling the isPlainObject() check
class SpecialClass {
  static {
    Object.setPrototypeOf(SpecialClass.prototype, null)
    SpecialClass.prototype.constructor = Object;
  }
}

isPlainObject(new SpecialClass()); // true

And, there could be other protections I could layer on there to try and prevent the spoofing, but it's a loosing game that I'm not wanting to dive too deep into :p


Some of the other _.isWhatever() functions that I'm thinking about are these:

function isMap(value) {
  try {
    Map.prototype.get.call(value, undefined);
    return true;
  } catch (error) {
    if (error instanceof TypeError) {
      return false;
    }
    throw error;
  }
}

function isSet(value) {
  try {
    Set.prototype.has.call(value, undefined);
    return true;
  } catch (error) {
    if (error instanceof TypeError) {
      return false;
    }
    throw error;
  }
}

function isArrayBuffer(value) {
  try {
    ArrayBuffer.prototype.slice.call(value, 0, 0);
    return true;
  } catch (error) {
    if (error instanceof TypeError) {
      return false;
    }
    throw error;
  }
}

// ...similar stuff for isDate, isWeakMap, isWeakSet, isTypedArray, etc

As far as I can tell, none of these can be spoofed (apart from monkey-patching the built-ins), which makes them stronger than Lodash's versions, since Lodash relies on Object.prototype.toString.call() for each of these. Though, some of the TypeError messages that get thrown worry me a bit. For the Date class, if I do Date.prototype.valueOf.call('this-is-not-a-date') in node, it gives me the error "this is not a Date object" - that makes me feel like this is pretty good for a date-class brand-check. But with Map.prototype.get.call('this-is-not-a-map', undefined) I instead get the error "Method Map.prototype.get called on incompatible receiver this-is-not-a-map", which makes it sound like it could potentially support other receivers besides map instances - but looking at the spec, Map.prototype.get.call() says it only accepts receivers that have the [[mapData]] internal slot, and the only way to install this internal slot onto an object, as far as I can tell, is by creating a Map instance - is it possible that platforms are allowed to create their own custom "Map" objects that also have the [[mapData]] internal slot? Am I reading too much into this error? (The same kind of "weaker error" seems to happen for Sets, typed arrays, WeakMap, and WeakSet)

I also found _.isRegExp() to be a bit tricky - it seems that every method the language provides for regular expressions is intended to be usable on any regexp-like object - none of them seem to provide brand-checking capabilities. So, the best I could come up with was this:

function isRegExp(value) {
  return RegExp(value) === value;
}

...which can be fooled, but seems to have a fair number of checks already built into it (like checking the .constructor property), so don't know if you can do much better than that.

Checking if something is a boolean object was another hard one - I don't believe there's a reliable cross-realm way to do that. I just resorted to using Object.prototype.toString.call(), but it feels icky everytime I do that.

RegExp is also tricky because that equality check fails for RegExp subclasses

const myReg = new class extends RegExp {}
console.log(RegExp(myReg) === myReg) // false

Ah, yes. And there doesn't really seem to be a way to work around that. So, perhaps I can also provide an inheritance-friendly version that simply does an Object.prototype.toString.call() check.

The problem with the built-in IsRegExp AO is that it can be easily duck-typed—anything with [@@match]() will work. Furthermore RegExp() also tests pattern.constructor === RegExp in addition to IsRegExp. What about:

function isRegExp(value) {
  if (typeof value !== "object") return false;
  try {
    "".includes(value);
  } catch {
    return true;
  }
  return false;
}

Doesn't work with a custom object that has a stringifier which throws, but neither does the RegExp(value) === value approach.

See https://github.com/inspect-js/is-regex/blob/89385885c2da891644e07220b4407f760c3dd629/index.js / https://npmjs.com/is-regex for the code that's actually required for this check across mostly all engine versions.

I'm looking at that code - what seems to be the heart of it is its usage of RegExp.prototype.exec.call(). Looking at the specs for that one, it seems like that will actually do a robust brand check?

  1. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]).

It's being called with an object that throws an error when stringified - I assume that's to try and prevent the lastIndex property on the regular expression from getting updated? Or something?

Most of the other stuff in there seems to be for supporting older or odd engines.

I'm curious about the check for the "lastIndex" property though - there's code in there that explicitly checks that this property exists on the regular expression - why is that needed?

Yep, that’s right.

Re lastIndex, see
https://github.com/inspect-js/is-regex/commit/d4a0a6b6774d962337251b8d68009f1a1e52c780

That commit shows it restoring the lastIndex property in a finally block, something it didn't do before, but that doesn't seem to be the commit that introduced the check for the lastIndex to begin with. I did find this commit I found that's (re-)introducing the check to see if lastIndex is on the regular expression, and in the commit message, it said it was done for performance reasons.