2
头图
How do you determine if a JavaScript native function is overridden? You can't -- or at least not reliably. There are some detection methods that come close, but you can't quite trust them.

JavaScript native functions

In JavaScript, a native function is a function whose source code has been compiled into native machine code. Native functions are available in JavaScript standard built-in objects (e.g. eval() , parseInt() etc.), as well as browser web APIs (e.g. fetch() , localStorage.getItem() etc.) found in.

Due to the dynamic nature of JavaScript, developers can override native functions exposed by the browser. This technique is called " monkey patching ".

monkey patch

Monkey patches are mainly used to modify the default behavior of browser built-in APIs and native functions. This is often the only way to add specific functionality, shim functionality, or connect APIs that you don't have access to.

For example, monitoring tools such as Bugsnag cover the Fetch and XMLHttpRequest APIs to gain visibility into network connections triggered by JavaScript code.

Monkey patching is a very powerful, but also very dangerous technique. Because the code you cover is not under your control: future updates to the JavaScript engine may break some assumptions in your patch, leading to serious bugs.

Also, by monkey-patching code that doesn't belong to you, you may be overwriting some code that has already been monkey-patched by other developers, introducing potential conflicts.

Based on this, sometimes you may need to test if a given function is native, or if it's been monkey-patched...but can you?

Use toString() to check

The most common way to check if a function is still "clean" (i.e. not monkey-patched) is to check its toString() output.

By default, the native function toString() will return something like "function fetch() { [native code] }" .

fetch-native-code.png

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

By monkey patching the native function, its toString() will stop returning the "[native code]" string, and instead return the stringified function body.

So an easy way to check if a function is still native is to check if its toString() output contains the "[native code]" string.

A preliminary check might 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 method works fine in most cases. However, you must know that it's easy to trick it into thinking a function is still native, which unfortunately isn't. Whether it's for malicious purposes (eg, dropping a virus in your code) or because you want your overrides to go undetected, there are several ways you can make a function appear "native".

For example, you can add some code (or even a comment) to the function body that contains the "[native code]" string:

 (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

Alternatively, you can 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

Alternatively, you can use bind to create a monkey-patch function to generate the 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

Alternatively, you can use an ES6 proxy to catch calls to apply() and monkey-patch the function:

 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

Well, I'll stop giving examples.

My point is: if you just check the function toString() , the developer can easily bypass the detection by monkey patching.

In my opinion, in most cases, you shouldn't care too much about the above edge cases. But if you care, you can try to override them with some extra checks.

For example:

  • You can use iframe to grab the "clean" value of toString() and use it in strict equality matching.
  • You can call multiple .toString().toString() to ensure that the function toString() is not overwritten.
  • Monkey-patch the Proxy constructor itself to determine if a native function is proxied (since by spec it should be impossible to detect if something is a Proxy).
  • and many more.

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

Grab clean function from iframe

If you need to call a "clean" function, rather than checking if a native function has been monkey-patched, another potential option is to grab it from a homologous iframe .

 // 创建一个同源iframe
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
// 新的iframe将创建自己的"干净"window对象,
// 所以你可以从那里抓取你感兴趣的函数。
const cleanFetch = iframe.contentWindow.fetch;

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

  • Whether it's because of a strong Content Security Policy (CSP) , or because your code isn't running in a browser, sometimes iframes may not work.
  • Although somewhat impractical, third parties can monkey patch the API of iframe . Therefore, you still cannot 100% trust the generated iframe window object of ---46dd80719652976ec875488f510dbfb9---.
  • Native functions that alter or use the DOM (like document.createElement ) won't work with this method, because they target the DOM of iframe , not the top-level one.

use congruence check

If safety is your primary concern, I think you should take a different approach: hold a reference to a "clean" native function and compare it later with a potentially monkey-patched function.

 <html>
  <head>
    <script>
    // 在任何其他脚本有机会修改原始的原生函数之前,存储一个引用。
    // 在这种情况下,我们只是持有一个原始fetchAPI的引用,并将其隐藏在一个闭包后面。
    // 如果你事先不知道你要检查什么API,你可能需要存储多个window对象的引用。
      (function () {
        const { fetch: originalFetch } = window;
        window.__isFetchMonkeyPatched = function () {
          return window.fetch !== originalFetch;
        };
      })();
      // 从现在开始,你可以通过调用window.__isFetchMonkeyPatched()
      // 来检查fetch API是否已经被猴子补丁过。
      //
      // 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 strict citation checking, we avoided all toString() vulnerabilities. It even works with proxies, since they cannot capture equality comparisons.

The main disadvantage of this approach is that it can be impractical. It requires storing the original function reference (to make sure it's still untouched) before running any other code in the application, and sometimes you won't be able to do that (for example, you're building a library).

There might be some way to break this method, but at the time of writing this, I don't know of it yet. If I'm missing something, please let me know.

How to determine if it is overwritten

My take on the problem (or better "guess") is that, depending on the use case, there may not be a fail proof way to determine it.

  • If you have control over the entire web page, you can set up your code ahead of time by storing references to the functions you want to check when they are still "clean" before doing the comparison.
  • Otherwise, if you can use iframe , you can create a hidden one-shot iframe , and grab a "clean" function from there -- knowing that you still can't 100% Make sure the API of iframe has not been monkey patched.
  • Otherwise, given the dynamic nature of JavaScript, you can use simple toString().includes("[native code]") checks, or add extensive security checks to cover most (but not all) edge cases.

Extended reading


chuck
300 声望41 粉丝