5

As I understand, the ES spec says that Proxy (global constructor for proxifying objects, functions and classes) is not detectable. It means that if I proxify a function, nobody who uses that proxified function can detect that I used Proxy. However, apparently I misunderstood it, becuase proxifying functions is detectable.

For example, new Proxy(a=>a,{apply:a=>a})+'' throws an error. It says

Uncaught TypeError: Function.prototype.toString requires that 'this' be a Function

However, typeof new Proxy(a=>a,{apply:a=>a}) is indeed "function", but it somehow fails to stringify the proxy. So, obviously, here is a situation when proxified function doesn't behave as non-proxified one should. Function.prototype.toString is able to distinguish proxified and non-proxified function.

My goal is to proxify a function such that it simple become undetectable. My first idea is to literally proxify the Proxy like so:

Proxy.toString = (a => () => a)(Proxy + '');
Proxy = new Proxy(Proxy, {
  construct: (f, args) => {
    if(typeof args[0] == 'function'){
      var a = args[0] + '';
      args[0].toString = () => a;
    }
    return new f(...args);
  }
});

But, sadly, this is detectable. If someone call Function.prototype.toString binded to my proxified function, the error will occur and he can therefore detect that my function is actually a proxy. So, I tried to proxify the Function and Function.prototype and also Function.prototype.toString, but then I realized I cannot proxify the Function because even if I override the global property Function, someone may access it using (a=>a).constructor.

So, this is why I am asking it here because I ran out of ideas. How to proxify a function to make it completelly undetectable? In the ES spec it explicitly says that "Proxy is undetectable", so as a side question, why is then proxifying a function detectable?

Edit

The reason I'm trying to achieve this is because I'm working on enhanced advertisement blocking extension for Chrome. I am dealing with very agressive website which exploits a huge amount of JavaScript tricks to detect if I'm viewing the ads or not. So, basically, I deleted an advertisement, and then their script checks if there is specific element, if not, then I cannot visit the website. So, I tried to proxify document.getElementById, but they check if it is proxified and if it is, I cannot visit the website, so I must make it undetectable.

  • Isnt the error thrown because arrow functions dont being their own this scope? – Sven van de Scheur Aug 15 '17 at 08:29
  • @SvenvandeScheur. I have no idea what you've asked. It has nothing to do with scopes. Once a function is proxified, the original one is not important anymore. Standard non-arrow functions throw the error too. –  Aug 15 '17 at 08:31
  • just as a remark, there is a related [SO question, trying to proxy an HTMLElement](https://stackoverflow.com/questions/56008415/how-to-create-a-proxy-for-htmlelement) . I wouldn't see it as a duplicate of this one, but discussions/answers on one should be relevant for the other. – Sebastian Nov 28 '22 at 15:19

2 Answers2

2

I have a couple of partial answers/remarks to this question:

1 . First of all: The behavior of the browsers seems to have changed in the meantime. Chrome 106 as well as Firefox 105 both don't throw the described type error for the initial example any more, but they still give a different return for the toString() method than the original object:

 new Proxy(a=>a, {apply:a=>a}) + ''

leads to the following toString() return value:

'function () { [native code] }'

while the original lambda expression (a=>a) + '' yields simply

'a=>a'

2 . The OP's attempt to change the toString of the proxy object is in fact changing the toString-method of the target args[0] (which is a=>a) by evaluating it always on itself via args[0] + '' (and thus eliminating any this). Therefore, when this redefined toString function of the target is invoked with the proxy as this argument, it doesn't make a difference anymore.

For anybody stumbling over this question on the search on how to assign a customized toString to a proxy (without the requirement of not being detectable), one should maybe mention the standard way to perform this without changing the target: It can be done with the get-trap of the handler, so extend the handler to:

{
    apply: a=>a,
    get(target, prop, receiver) {
       if(prop == "toString") {
          return target.toString.bind(target);
       } else {
          return Reflect.get(target, prop, receiver); // or your favorite forwarding
       }
    }
}

This also yields the same toString() answer for proxy and target. The 'else'-case can be made more useful, by binding also other function properties in general to the target, at least if they were not explicitly bound to a different receiver (see private property forwarding).

3 . The OP's problem of a replaced global Function can be overcome, however. At least I think so. There is indeed a reference to the original Function constructor on its instances:

(a=>a).constructor === Function; // true

But one doesn't need to replace Function itself, only its properties, or to be more precise not even those, but only one of its properties' properties. Consider the following simple redefinition. It is not very sophisticated (targeted to only one single proxy), but does the job at least for the toString property of this proxy:

const proxy = new Proxy(a=>a,{apply:a=>a});
{
    const nativeToString = Function.prototype.toString;
    Function.prototype.toString = function() {
        return this === proxy? "a=>a" : nativeToString.call(this);
    }
}

Now at least this test is successful:

 (x=>x).constructor.prototype.toString.call(proxy); // returns "a=>a" 

4 . However, as @jmrk pointed out in his answer, the reason for the detectability of the proxy is the existence of some special internal slots, which cannot be intercepted via the proxy handler traps. The latter intercept only the standard object internal methods. The task is thus to find all ways of triggering these internal slots via external javascript calls and then override corresponding globally accessible functions, as was done with toString() above. I'm not sure about further ways to trigger internal slots of Function, but the same problem occurs also when trying to use a proxy as replacement for an HTMLElement, let's call it htmlProxy. In that case the invocation getComputedStyle(htmlProxy) throws a type error even if htmlProxy identifies as HTMLElement. Similarly element.insertNode(htmlElementProxy) throws an exception. It seems that one would need to redefine all these globally accessible trigger-functions to make the proxy undetectable.

Internal slots can be seen as private properties of built-in objects. Therefore it is not surprising that the same issue appears with private properties of non-built-in classes/objects. One can use the already above (in point 2) mentioned private property forwarding to get the same behavior of the proxy as of the target object when methods which access private properties are called via the proxy. Let's say we have someObject of type SomeClass with a private member #memberund a public getter getMember. Then we can make the proxy of someObject calling successfully this function by forwarding also this to the target, so that proxy.getMember() will just give the member of someObject. However, if somebody is calling SomeClass.prototype.getMember.call(proxy), there will again be an exception and the only way around is redefining SomeClass.prototype.getMember.

Sebastian
  • 365
  • 3
  • 17
1

I don't think what you're trying to do is possible with Proxies. The spec for Function.prototype.toString clearly defines the TypeError-throwing behavior. Since there's no way to give a Proxy an [[ECMAScriptCode]] "internal slot", it'll always throw when called on a Proxy.

I also see no mention of any "Proxy is undetectable" statement in the spec; the string 'detectable' doesn't show up anywhere in the document. Where did you find this claim?

Maybe you can overwrite functions (and their .toString properties) to achieve your goal? Roughly:

var original_getElementById = document.getElementById;
document.getElementById = function(id) {
  if (...) {
    return original_getElementById(id);
  } else {
    // special handling here
  }
}
jmrk
  • 34,271
  • 7
  • 59
  • 74
  • (+1) for the internal slot mentioning. As the proxy handler is an interceptor for the ["object internal methods"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#object_internal_methods) (`Reflect` functions), it seems fair to say that a proxy is not detectable for "ordinary" objects, i.e objects that don't have more internal slots than those standard "object internal methods". Right? – Sebastian Nov 28 '22 at 15:32