Checking if a JavaScript native function is monkey patched

#javascript, #webdev
Jul 30, 2022

TL;DR: How can you determine whether a JavaScript native function was overridden?
You can’t — or at least not reliably. There are ways to get close to it, but you can’t fully trust them.

Native functions in JavaScript

In JavaScript, a “native function” is a function whose source code has been compiled into native machine code. Native functions can be found in JavaScript’s standard built-in objects (such as eval(), parseInt(), etc.) and browsers Web API (such as fetch(), localStorage.getItem(), etc.).

Because of the dynamic nature of JavaScript, developers can override native functions exposed by the browser. This technique is known as monkey patching.

Monkey patching

Monkey patching is mainly used to modify the default behavior of the browser’s built-in APIs and native functions. This is often the only way to add specific functionalities, polyfill features, or hook into APIs that you wouldn’t be able to access otherwise.
For example, monitoring tools such as Bugsnag override the Fetch and XMLHttpRequest APIs to gain visibility into network connections triggered by JavaScript code.

Monkey patching is a powerful but dangerous technique because the code you’re overriding is not in your control: future updates to the JavaScript engine may break some assumptions made in your patch and cause serious bugs.
Additionally, by monkey patching code you don’t own, you might override some code that has already been monkey patched by another developer, introducing a potential conflict.

For these (and many other) reasons, sometimes you might need to test if a given function is a native function or if it’s monkey patched… but can you?

Using toString() to check if a function is monkey patched

The most common way to check if a function is still “clean” (as in non-monkey-patched) is by checking the output of its toString().
By default, a native function toString() returns something along the line of "function fetch() { [native code] }":

fetch native code

This string can slightly vary depending on what JavaScript engine is running. Still, in most browsers you can safely assume that this string will include the "[native code]" substring.

By monkey patching a native function, its toString() will return stop returning the "[native code]" string in favor of returning the stringified function body.
Therefore, a simple way to check if a function is still native is by checking if its toString() output contains the "[native code]" string.

A rudimentary check may look like this:

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

isNativeFunction(window.fetch); // → true

// Monkey patch the fetch API
(function () {
  const { fetch: originalFetch } = window;
  window.fetch = function fetch(...args) {
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
  };
})();

window.fetch.toString(); // → "function fetch(...args) {\n console.log("Fetch...

isNativeFunction(window.fetch); // → false

This approach works fine in most scenarios. However, you must know that it’s easy to trick it into thinking a function is still native when it’s not. Be it for malicious intent (e.g., to poison the code) or because you want your override to say undetected, there are several ways you can make the function looks “native”.

For example, you could add some code (or even a comment!) in the function body containing the "[native code]" string in it:

(function () {
  const { fetch: originalFetch } = window;
  window.fetch = function fetch(...args) {
    // 🧌 function fetch() { [native code] }
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
  };
})();

window.fetch.toString(); // → "function fetch(...args) {\n // 🧌 function fetch...

isNativeFunction(window.fetch); // → true

…or you could override the toString() method to return a string containing "[native code]":

(function () {
  const { fetch: originalFetch } = window;
  window.fetch = function fetch(...args) {
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
  };
})();

window.fetch.toString = function toString() {
  return `function fetch() { [native code] }`;
};

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

isNativeFunction(window.fetch); // → true

…or you could create the monkey patched function using bind, which generates a native function:

(function () {
  const { fetch: originalFetch } = window;
  window.fetch = function fetch(...args) {
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
  }.bind(window.fetch); // 👈
})();

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

isNativeFunction(window.fetch); // → true

…or you could monkey patch the function by trapping apply() calls with a ES6 proxy — which would make the function look native from the outside:

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

Ok, I’ll stop with the examples.
My point is: developers can easily make a monkey patch fly under the radar if you’re just checking the function’s toString().

I think that, most often than not, you shouldn’t care too much about the edge cases above. But if you do, you can try covering them with some additional checks.
For example:

  • you can use throwaway iframes to grab the “clean” value of a toString() and use it in a strict equality match;
  • you can invoke multiple .toString().toString() to ensure the function toString() isn’t overridden;
  • you can put on your metaprogrammer hat and monkey patch the Proxy constructor itself to determine whether a native function is proxied (because by following the specs, it should be impossible to detect if something is a Proxy);
  • etc.

It all depends on how deep you want to go into the toString() rabbit hole.
But is it worth it? Can you really cover all the edge cases?

Grabbing a clean function from an iframe

If your need is to invoke a “clean” function — instead of checking if a native function is monkey patched — another potential option is to grab it from a same-origin iframe:

// Create a new same-origin iframe.
// You'll probably want to add some styling for hiding it, and eventualy remove
// it from the DOM later on.
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
// The new iframe will create its own "clean" window object, so you can grab
// the function you're interested in from there.
const cleanFetch = iframe.contentWindow.fetch;

While I think this approach is still better than validating a function using toString(), it still has some significant limitations:

  • Be it because of a strong Content Security Policy (CSP) or because your code is not running within a browser, sometimes iframes may not be available.
  • Although a bit impractical, a third-party could monkey path the iframe API. So you still can’t 100% trust the window object of the generated iframe.
  • Native functions that alter or use the DOM (such as document.createElement) won’t work with this approach because they’ll target the iframe DOM instead of the top-level one.

This solution was suggested in a lobster.rs thread.

Using referential equality to check if a function is monkey patched

If safety is your first concern, I think you should go with a different approach: 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).

There might be some ways to break this approach, but at the time of writing I’m not aware of any of them. Let me know if I’m missing something!

So, how can you determine whether a JavaScript native function was overridden?

I can count on the fingers of one hand the number of times I needed to check if a function was monkey patched.

Still, my take (or better, “guess”) on the subject is that, depending on the use case, there might not be a fail proof way to determine it.

  • If you’re in control of the entire web page, you can set up your code in advance by storing references of the functions you want to check when they’re still “clean” and compare them later.
  • Otherwise, if you can use iframes, you can create a hidden disposable iframe and grab a “clean” function from there — knowing you still can’t be 100% sure the iframe API isn’t monkey patched.
  • Otherwise, given the dynamic nature of JavaScript, you can either use a simple toString().includes("[native code]") check (knowing malicious actors can easily fly under the radar), or add a ton of safety checks to cover most (but not all) edge cases.

Further reading & related resources