乐趣区

关于javascript:检查原生-JavaScript-函数是否被覆盖

你如何确定一个 JavaScript 原生函数是否被笼罩?你不能 – 或者至多无奈牢靠地确定。有一些检测办法很靠近,但你不能齐全置信它们。

JavaScript 原生函数

在 JavaScript 中,原生函数指的是其源代码曾经被编译进原生机器码的函数。原生函数能够在 JavaScript 规范内置对象(比如说 eval()parseInt() 等等),以及浏览器 Web API(比如说 fetch()localStorage.getItem() 等等)中找到。

因为 JavaScript 的动静个性,开发者能够笼罩浏览器裸露的原生函数。这种技术被称为 ” 猴子补丁 ”。

猴子补丁

猴子补丁次要用于批改浏览器内置 API 和原生函数的默认行为。这通常是增加特定性能、垫片性能或连贯你无法访问的 API 的惟一路径。

比如说,诸如 Bugsnag 等监控工具笼罩了 FetchXMLHttpRequest APIs,以取得对由 JavaScript 代码触发的网络连接的可见性。

猴子补丁是十分弱小,但也是十分危险的技术。因为你所笼罩的代码不受你的管制:将来对 JavaScript 引擎的更新可能会突破你的补丁中的一些假如,从而导致重大的 bug。

此外,通过对不属于你的代码进行猴子补丁,你可能会笼罩一些曾经被其余开发者猴子补丁过的代码,从而引入潜在的抵触。

基于此,有时你可能须要测试一个给定的函数是否为原生函数,或者它是否被猴子补丁过 … 但你能做到吗?

应用 toString() 查看

查看一个函数是否依然是 “ 洁净的 ”(如未被猴子补丁)的最罕用办法是查看其 toString() 的输入。

默认状况下,原生函数的 toString() 会返回相似于 "function fetch() { [native code] }"的内容。

这个字符串可能略有不同,这取决于运行的是什么 JavaScript 引擎。不过,在大多数浏览器中,你能够平安地认为这个字符串将包含 "[native code]" 子串。

通过对原生函数进行猴子补丁,它的 toString() 将进行返回 "[native code]" 字符串,而是返回字符串化的函数体。

因而,查看一个函数是否依然是原生的一个简略办法是,查看其 toString() 输入是否蕴含 "[native code]" 字符串。

初步查看可能是这样的:

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

这种办法在大多数状况下都能失常工作。然而,你必须晓得,坑骗它是很容易的,让它认为一个函数依然是原生的,惋惜并不是。无论是出于歹意(例如,在代码中下病毒),还是因为你想让你的笼罩不被发现,你有几种办法能够让函数看起来是 ” 原生 ” 的。

比如说,你能够在函数体中增加一些代码(甚至能够是正文),其中蕴含 "[native code]" 字符串:

(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

或者,你能够笼罩 toString() 办法,让其返回一个蕴含 "[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

或者,你能够应用 bind 创立一个猴子补丁函数,来生成原生函数:

(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

或者,你能够用 ES6 代理来捕捉 apply() 的调用,对该函数进行猴子补丁:

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

好了,我将进行举例。

我的观点是:如果你只是查看函数的toString(),开发者很容易通过猴子补丁来绕过检测。

我认为,在大多数状况下,你不应该太在意上述的边缘状况。但如果你在乎,你能够尝试用一些额定的查看来笼罩它们。

比如说:

  • 你能够应用 iframe 来抓取 toString() 的 ” 洁净 ” 值,并在严格的相等匹配中应用它。
  • 你能够调用多个 .toString().toString() 以确保函数 toString() 不被重写。
  • 用猴子补丁 Proxy 构造函数自身,以确定一个原生函数是否被代理了(因为依照标准,应该不可能检测到某物是否是 Proxy)。
  • 等等。

这齐全取决于你想在 toString() 的兔子洞里走多深(爱丽丝梦游仙境)。但这值得吗?你真的能笼罩所有的边缘状况吗?

从 iframe 中抓取洁净函数

如果你须要调用一个 ” 洁净 ” 函数,而不是查看一个原生函数是否被猴子补丁过,另一个潜在的抉择是从一个同源的 iframe 中抓取它。

// 创立一个同源 iframe
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
// 新的 iframe 将创立本人的 "洁净"window 对象,// 所以你能够从那里抓取你感兴趣的函数。const cleanFetch = iframe.contentWindow.fetch;

尽管我认为这种办法依然比应用 toString() 验证一个函数好,但它依然有一些显著的局限性:

  • 无论是因为弱小的 Content Security Policy (CSP),还是因为你的代码没有在浏览器中运行,有时 iframes 可能无奈应用。
  • 尽管有点不切实际,但第三方能够对 iframe 的 API 进行猴子补丁。因而,你依然不能 100% 地信赖生成的 iframewindow对象。
  • 扭转或应用 DOM 的原生函数(如 document.createElement)将无奈应用这种办法,因为它们的指标是iframe 的 DOM,而不是顶层的。

应用全等查看

如果平安是你首要思考的因素,我认为你应该采纳不同的办法:持有一个 ” 洁净 ” 原生函数的援用,稍后用潜在的猴子补丁函数与它进行比拟。

<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>

通过应用严格的援用查看,咱们防止了所有 toString() 的破绽。它甚至实用于代理,因为它们不能捕捉相等比拟。

这种办法的次要毛病是,它可能不切实际。它要求在运行应用程序中的任何其余代码之前存储原始函数援用(以确保它依然未被涉及),有时你将无奈做到这一点(例如,你正在构建一个库)。

可能有一些办法能够突破这种办法,但在写这篇文章的时候,我还不晓得这种办法。如果我脱漏了什么,请让我通晓。

如何确定是否被笼罩

我对这个问题的认识(或者更好的说法是 “ 猜想 ”)是,依据不同的应用状况,可能没有一种失败的证实办法来确定它。

  • 如果你能管制整个网页,当它们依然是 ” 洁净的 ” 时候,你能够通过存储你想查看的函数的援用,来提前设置你的代码,而后再进行比拟。
  • 否则,如果你能应用 iframe,你能够创立一个暗藏的一次性iframe,并从那里抓取一个 ” 洁净 “ 的函数 – 要晓得你依然不能 100% 确定iframe 的 API 没有被猴子补丁过。
  • 否则,思考到 JavaScript 的动静性质,你能够应用简略的 toString().includes("[native code]") 查看,或者增加大量的安全检查来笼罩大多数(但不是全副)边缘状况。

扩大浏览

  • StackOverflow: Is there a way to check if a native Javascript function was monkey patched?
  • StackOverflow: Detect if function is native to browser
  • StackOverflow: [How to determine that a JavaScript function is native (without testing‘[native code]‘)](https://stackoverflow.com/que…)
  • David Walsh: Detect if a Function is Native Code with JavaScript
退出移动版