Detect if function is native to browser

Viewed 8221

I am trying to iterate over all the globals defined in a website, but in doing so I am also getting the native browser functions.

var numf=0; var nump=0; var numo=0; 
for(var p in this) { 
    if(typeof(this[p]) === "function"){
        numf+=1;
        console.log(p+"()");
    } else if(typeof p != 'undefined'){
        nump+=1;
        console.log(p);
    } else { 
        numo+=1;
        console.log(p);
    }
}

Is there a way to determine if a function is native to the browser or created in a script?

7 Answers

Function.prototype.toString can be spoofed, something kinda like this:

Function.prototype.toString = (function(_toString){
  return function() {
    if (shouldSpoof) return 'function() { [native code] }'
    return _toString.apply(this, arguments)
  }
})(Function.prototype.toString)

You can detect if Function.prototype.toString is vandalized by trapping .apply(), .call(), .bind() (and others).

And if it was, you can grab a "clean" version of Function.prototype.toString from a newly injected IFRAME.

2022 answer

Now that we have the Proxy API, there's no fail proof way to determine if a native function was overridden or not.

For example:

function isNativeFunction(f) {
  return f.toString().includes("[native code]");
}

window.fetch = new Proxy(window.fetch, {
  apply: function (target, thisArg, argumentsList) {
    console.log("Fetch call intercepted:", ...argumentsList);
    Reflect.apply(...arguments);
  },
});

window.fetch.toString(); // → "function fetch() { [native code] }"

isNativeFunction(window.fetch); // → true

Following the specs, a proxied object should be indistinguishable from its target. Some runtimes (e.g., Node.js) offers some utilities to go against the spec and check if an object is proxied, but in the browser the only way to do so is by monkey patching the Proxy API itself before any proxy is applied.

So, back to the original question — I think the only available option nowadays is to hold a reference of the “clean” native function and, later on, compare your potentially monkey patched function with it:

<html>
  <head>
    <script>
      // Store a reference of the original "clean" native function before any
      // other script has a chance to modify it.
      // In this case, we're just holding a reference of the original fetch API
      // and hide it behind a closure. If you don't know in advance what API
      // you'll want to check, you might need to store a reference to multiple
      // `window` objects.
      (function () {
        const { fetch: originalFetch } = window;
        window.__isFetchMonkeyPatched = function () {
          return window.fetch !== originalFetch;
        };
      })();
      // From now on, you can check if the fetch API has been monkey patched
      // by invoking window.__isFetchMonkeyPatched().
      //
      // Example:
      window.fetch = new Proxy(window.fetch, {
        apply: function (target, thisArg, argumentsList) {
          console.log("Fetch call intercepted:", ...argumentsList);
          Reflect.apply(...arguments);
        },
      });
      window.__isFetchMonkeyPatched(); // → true
    </script>
  </head>
</html>

By using a strict reference check, we avoid all toString() loopholes. And it even works on proxies because they can’t trap equality comparisons.

The main drawback of this approach is that it can be impractical. It requires storing the original function reference before running any other code in your app (to ensure it’s still untouched), which sometimes you won’t be able to do (e.g., if you’re building a library).

For more info, I recently wrote an article going in-depth into the available approaches for determining if a native function was monkey patched. You can find it here.

For developers who want to use these detection methods to block user behavior (such as userscript), there are actually ways to bypass these methods.

For Example:
(The detection method for the isNative function below comes from methods provided by others in the past.)

function isNative(f) {
    if (!!/bound/.test(f.name)) return 1;
    if (!!!/\{\s+\[native code\]/.test(Function.prototype.toString.call(f))) return 2;
    if (!!!/\{\s+\[native code\]/.test(eval(f) + "")) return 3;
    if ((typeof f).toLowerCase() !== 'function') return 4;
    if (!/^\s*function\s*(\b[a-z$_][a-z0-9$_]*\b)*\s*\((|([a-z$_][a-z0-9$_]*)(\s*,[a-z$_][a-z0-9$_]*)*)\)\s*{\s*\[native code\]\s*}\s*$/i.test(String(f))) return 5;

    return true;
}

function fakeStringify(value) {
    return `Fake Stringify ${value}`;
};

const s = new Proxy(fakeStringify, {
    get(target, prop, receiver) {
        if (prop === "name") {
            return "stringify";
        } else if (prop === Symbol.toPrimitive) {
            return function () {
                return "function () { [native code] }";
            };
        }
    }
});

const obj = {
    a: 1
};
console.log("========= [native] =========");
console.log(isNative(JSON.stringify));
console.log(JSON.stringify(obj));
console.log(JSON.stringify.name);

JSON.stringify = s;

console.log("======== [override] ========");
console.log(isNative(JSON.stringify));
console.log(JSON.stringify(obj));
console.log(JSON.stringify.name);

After execution, it can be found that the isNative detection can be successfully deceived after passing through the Proxy.

As far as I know, there is no way to detect Proxy fucntion in the browser environment. But if you know how to detect it, please provide it, thanks!

Related