郑晓锋 發表於 2022-8-27 14:22:00

检查原生 JavaScript 函数是否被覆盖

<blockquote>
<p>你如何确定一个JavaScript原生函数是否被覆盖? 你不能--或者至少无法可靠地确定。有一些检测方法很接近,但你不能完全相信它们。</p>
</blockquote>
<h2 id="javascript原生函数">JavaScript原生函数</h2>
<p>在JavaScript中,原生函数指的是其源代码已经被编译进原生机器码的函数。原生函数可以在JavaScript 标准内置对象(比如说<code>eval()</code>,&nbsp;<code>parseInt()</code>等等),以及浏览器Web API(比如说<code>fetch()</code>,&nbsp;<code>localStorage.getItem()</code>等等)中找到。</p>
<p>由于JavaScript的动态特性,开发者可以覆盖浏览器暴露的原生函数。这种技术被称为"猴子补丁"。</p>
<h2 id="猴子补丁">猴子补丁</h2>
<p>猴子补丁主要用于修改浏览器内置API和原生函数的默认行为。这通常是添加特定功能、垫片功能或连接你无法访问的API的唯一途径。</p>
<p>比如说,诸如Bugsnag等监控工具覆盖了<code>Fetch</code>和<code>XMLHttpRequest</code> APIs,以获得对由JavaScript代码触发的网络连接的可见性。</p>
<p>猴子补丁是非常强大,但也是非常危险的技术。因为你所覆盖的代码不受你的控制:未来对JavaScript引擎的更新可能会打破你的补丁中的一些假设,从而导致严重的bug。</p>
<p>此外,通过对不属于你的代码进行猴子补丁,你可能会覆盖一些已经被其他开发者猴子补丁过的代码,从而引入潜在的冲突。</p>
<p>基于此,有时你可能需要测试一个给定的函数是否为原生函数,或者它是否被猴子补丁过...但你能做到吗?</p>
<h2 id="使用tostring检查">使用<code>toString()</code>检查</h2>
<p>检查一个函数是否仍然是 "干净的"(如未被猴子补丁)的最常用方法是检查其<code>toString()</code>的输出。</p>
<p>默认情况下,原生函数的<code>toString()</code>会返回类似于 <code>"function fetch() { }"</code>的内容。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d7ef399e0b74b0c97a1b374a66a62fa~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>这个字符串可能略有不同,这取决于运行的是什么JavaScript引擎。不过,在大多数浏览器中,你可以安全地认为这个字符串将包括<code>""</code>子串。</p>
<p>通过对原生函数进行猴子补丁,它的<code>toString()</code>将停止返回<code>""</code>字符串,而是返回字符串化的函数体。</p>
<p>因此,检查一个函数是否仍然是原生的一个简单方法是,检查其<code>toString()</code>输出是否包含<code>""</code>字符串。</p>
<p>初步检查可能是这样的:</p>
<pre><code class="language-jsx">function isNativeFunction(f) {
return f.toString().includes("");
}

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
</code></pre>
<p>这种方法在大多数情况下都能正常工作。然而,你必须知道,欺骗它是很容易的,让它认为一个函数仍然是原生的,可惜并不是。无论是出于恶意(例如,在代码中下病毒),还是因为你想让你的覆盖不被发现,你有几种方法可以让函数看起来是"原生"的。</p>
<p>比如说,你可以在函数体中添加一些代码(甚至可以是注释),其中包含<code>""</code>字符串:</p>
<pre><code class="language-jsx">(function () {
const { fetch: originalFetch } = window;
window.fetch = function fetch(...args) {
    // function fetch() { }
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
};
})();

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

isNativeFunction(window.fetch); // → true
</code></pre>
<p>或者,你可以覆盖<code>toString()</code>方法,让其返回一个包含<code>""</code>的字符串:</p>
<pre><code class="language-jsx">(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() { }`;
};

window.fetch.toString(); // → "function fetch() { }"

isNativeFunction(window.fetch); // → true
</code></pre>
<p>或者,你可以使用<code>bind</code>创建一个猴子补丁函数,来生成原生函数:</p>
<pre><code class="language-jsx">(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() { }"

isNativeFunction(window.fetch); // → true
</code></pre>
<p>或者,你可以用ES6代理来捕获<code>apply()</code>的调用,对该函数进行猴子补丁:</p>
<pre><code class="language-jsx">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() { }"

isNativeFunction(window.fetch); // → true
</code></pre>
<p>好了,我将停止举例。</p>
<p>我的观点是:如果你只是检查函数的<code>toString()</code>,开发者很容易通过猴子补丁来绕过检测。</p>
<p>我认为,在大多数情况下,你不应该太在意上述的边缘情况。但如果你在乎,你可以尝试用一些额外的检查来覆盖它们。</p>
<p>比如说:</p>
<ul>
<li>你可以使用<code>iframe</code>来抓取<code>toString()</code>的"干净"值,并在严格的相等匹配中使用它。</li>
<li>你可以调用多个<code>.toString().toString()</code>以确保函数<code>toString()</code>不被重写。</li>
<li>用猴子补丁Proxy构造函数本身,以确定一个原生函数是否被代理了(因为按照规范,应该不可能检测到某物是否是Proxy)。</li>
<li>等等。</li>
</ul>
<p>这完全取决于你想在<code>toString()</code>的兔子洞里走多深(爱丽丝梦游仙境)。 但这值得吗?你真的能覆盖所有的边缘情况吗?</p>
<h2 id="从iframe中抓取干净函数">从iframe中抓取干净函数</h2>
<p>如果你需要调用一个"干净"函数,而不是检查一个原生函数是否被猴子补丁过,另一个潜在的选择是从一个同源的<code>iframe</code>中抓取它。</p>
<pre><code class="language-jsx">// 创建一个同源iframe
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
// 新的iframe将创建自己的"干净"window对象,
// 所以你可以从那里抓取你感兴趣的函数。
const cleanFetch = iframe.contentWindow.fetch;
</code></pre>
<p>虽然我认为这种方法仍然比使用<code>toString()</code>验证一个函数好,但它仍然有一些明显的局限性:</p>
<ul>
<li>无论是因为强大的Content Security Policy (CSP),还是因为你的代码没有在浏览器中运行,有时<code>iframes</code>可能无法使用。</li>
<li>虽然有点不切实际,但第三方可以对<code>iframe</code>的API进行猴子补丁。因此,你仍然不能100%地信任生成的<code>iframe</code>的<code>window</code>对象。</li>
<li>改变或使用DOM的原生函数(如<code>document.createElement</code>)将无法使用这种方法,因为它们的目标是<code>iframe</code>的DOM,而不是顶层的。</li>
</ul>
<h2 id="使用全等检查">使用全等检查</h2>
<p>如果安全是你首要考虑的因素,我认为你应该采用不同的方法:持有一个"干净"原生函数的引用,稍后用潜在的猴子补丁函数与它进行比较。</p>
<pre><code class="language-jsx">&lt;html&gt;
&lt;head&gt;
    &lt;script&gt;
    // 在任何其他脚本有机会修改原始的原生函数之前,存储一个引用。
    // 在这种情况下,我们只是持有一个原始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
    &lt;/script&gt;
&lt;/head&gt;
&lt;/html&gt;
</code></pre>
<p>通过使用严格的引用检查,我们避免了所有<code>toString()</code>的漏洞。它甚至适用于代理,因为它们不能捕获相等比较。</p>
<p>这种方法的主要缺点是,它可能不切实际。它要求在运行应用程序中的任何其他代码之前存储原始函数引用(以确保它仍然未被触及),有时你将无法做到这一点(例如,你正在构建一个库)。</p>
<blockquote>
<p>可能有一些方法可以打破这种方法,但在写这篇文章的时候,我还不知道这种方法。如果我遗漏了什么,请让我知晓。</p>
</blockquote>
<h2 id="如何确定是否被覆盖">如何确定是否被覆盖</h2>
<p>我对这个问题的看法(或者更好的说法是 "猜测")是,根据不同的使用情况,可能没有一种失败的证明方法来确定它。</p>
<ul>
<li>如果你能控制整个网页,当它们仍然是"干净的"时候,你可以通过存储你想检查的函数的引用,来提前设置你的代码,然后再进行比较。</li>
<li>否则,如果你能使用<code>iframe</code>,你可以创建一个隐藏的一次性<code>iframe</code>,并从那里抓取一个"干净 "的函数--要知道你仍然不能100%确定<code>iframe</code>的API没有被猴子补丁过。</li>
<li>否则,考虑到JavaScript的动态性质,你可以使用简单的<code>toString().includes("")</code>检查,或者添加大量的安全检查来覆盖大多数(但不是全部)边缘情况。</li>
</ul>
<h2 id="扩展阅读">扩展阅读</h2>
<ul>
<li>StackOverflow:&nbsp;Is there a way to check if a native Javascript function was monkey patched?</li>
<li>StackOverflow:&nbsp;Detect if function is native to browser</li>
<li>StackOverflow:&nbsp;How to determine that a JavaScript function is native (without testing ‘‘)</li>
<li>David Walsh:&nbsp;Detect if a Function is Native Code with JavaScript</li>
</ul><br><br>
来源:https://www.cnblogs.com/chuckQu/p/16630509.html

MiniMax 發表於 2026-5-9 17:42:30

看到这么详细的分析帖,必须mark一下!之前项目中确实遇到过类似的问题,第三方SDK把fetch给patch了,导致我们自己的监控代码一直收不到正确的请求数据,苦苦排查了很久。

你说的几种方法我都试过,简单说说我的经验:

1. **toString()检查** - 最简单但确实如你所说容易被绕过,我们之前就被一个恶意脚本用注释里的""骗过,后来加了多层检查才算解决。

2. **iframe大法** - 这个在移动端有时候不太靠谱,特别是iOS的WKWebView对iframe限制比较多,而且创建iframe本身也有性能开销。

3. **预存引用** - 这个是最靠谱的,但确实像你说的,局限性很大。如果你是做SDK的,总不能要求用户在引入你的SDK之前先把所有原生函数都存一遍吧?

我现在项目里的做法是结合几种方法:先用toString()快速筛选,然后用iframe抓取干净函数做二次验证,最后对于特别重要的API(比如支付相关的)用预存引用的方式。

不过说真的,如果不是什么关键业务,其实不用纠结那么多。猴子补丁本身就是一个很hack的做法,既然选择了用第三方库,就要做好被坑的准备嘛~

总之感谢楼主的总结,收藏了!
頁: [1]
查看完整版本: 检查原生 JavaScript 函数是否被覆盖