检查原生 JavaScript 函数是否被覆盖
你如何确定一个JavaScript原生函数是否被覆盖? 你不能--或者至少无法可靠地确定。有一些检测方法很接近,但你不能完全相信它们。
JavaScript原生函数
在JavaScript中,原生函数指的是其源代码已经被编译进原生机器码的函数。原生函数可以在JavaScript 标准内置对象(比如说eval(), parseInt()等等),以及浏览器Web API(比如说fetch(), localStorage.getItem()等等)中找到。
由于JavaScript的动态特性,开发者可以覆盖浏览器暴露的原生函数。这种技术被称为"猴子补丁"。
猴子补丁
猴子补丁主要用于修改浏览器内置API和原生函数的默认行为。这通常是添加特定功能、垫片功能或连接你无法访问的API的唯一途径。
比如说,诸如Bugsnag等监控工具覆盖了Fetch和XMLHttpRequest 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%地信任生成的iframe的window对象。 - 改变或使用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]‘)
- David Walsh: Detect if a Function is Native Code with JavaScript
检查原生 JavaScript 函数是否被覆盖的更多相关文章
- 100个常用的原生JavaScript函数
1.原生JavaScript实现字符串长度截取 复制代码代码如下: function cutstr(str, len) { var temp; var icount = 0; var ...
- C#构造方法(函数) C#方法重载 C#字段和属性 MUI实现上拉加载和下拉刷新 SVN常用功能介绍(二) SVN常用功能介绍(一) ASP.NET常用内置对象之——Server sql server——子查询 C#接口 字符串的本质 AJAX原生JavaScript写法
C#构造方法(函数) 一.概括 1.通常创建一个对象的方法如图: 通过 Student tom = new Student(); 创建tom对象,这种创建实例的形式被称为构造方法. 简述:用来初 ...
- [转] 有趣的JavaScript原生数组函数
在JavaScript中,可以通过两种方式创建数组,Array构造函数和 [] 便捷方式, 其中后者为首选方法.数组对象继承自Object.prototype,对数组执行typeof操作符返回‘obj ...
- JavaScript原生数组函数
有趣的JavaScript原生数组函数 在JavaScript中,可以通过两种方式创建数组,构造函数和数组直接量, 其中后者为首选方法.数组对象继承自Object.prototype,对数组执行typ ...
- 有趣的JavaScript原生数组函数
本文由 伯乐在线 - yanhaijing 翻译.未经许可,禁止转载!英文出处:flippinawesome.欢迎加入翻译小组. 在JavaScript中,可以通过两种方式创建数组,Array构造函数 ...
- 原生JavaScript实现函数的防抖和节流
原生JavaScript实现函数的防抖和节流 参考:https://www.jianshu.com/p/c8b86b09daf0 想详细了解的直接戳上面链接了,讲得非常清楚.下面只给代码和我自己写的注 ...
- [转]WEB开发者必备的7个JavaScript函数
我记得数年前,只要我们编写JavaScript,都必须用到几个常用的函数,比如,addEventListener 和 attachEvent,并不是为了很超前的技术和功能,只是一些基本的任务,原因是各 ...
- WEB开发者必备的7个JavaScript函数
防止高频调用的debounce函数 这个 debounce 函数对于那些执行事件驱动的任务来说是必不可少的提高性能的函数.如果你在使用scroll, resize, key*等事件触发执行任务时不使用 ...
- JavaScript常用,继承,原生JavaScript实现classList
原文链接:http://caibaojian.com/8-javascript-attention.html 基于 Class 的组件最佳实践(Class Based Components) 基于 C ...
随机推荐
- C#.NET读取文本文件的几种办法
一次读取一个字符 //文件路径 string filePath = @"C:\Users\Administrator\Downloads\test\test.txt"; //文本读 ...
- 基于web3D展示技术的煤矿巷道3D可视化系统
地下开采离不开巷道工程.煤矿的生产.运输.排水.通风等各个环节都少不了巷道的支持.在煤矿智能化建设被提上日程的今天,巷道工程的智能化.可视化建设也成了行业趋势.尤其是复杂的井下作业环境,人员信息安全问 ...
- node线上项目连接mysql出现 504 Gateway Time-Out
var connection = mysql.createConnection({host : 'localhost',user : 'root',password : '123456',port: ...
- 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用
欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...
- 广东省30m二级分类土地利用数据(矢量)
数据下载链接:百度云下载链接 广东省,地处中国大陆最南部,属于东亚季风区,从北向南分别为中亚热带.南亚热带和热带气候,是中国光.热和水资源最丰富的地区之一.主要河系为珠江的西江.东江.北江和三角洲水 ...
- 集合容器和Hash表
集合容器 集合相当于一个容器,在我们使用Arraylist的时候添加参数相当与放了一个容器中.这里面的元素是可以重复的 在HashSet中添加元素是没有重复的,我们来写一个测试看一下 public s ...
- 新版recon-ng安装模块
表现:进入后报[*] No modules enabled/installed. 1.直接在recon-ng控制台输入marketplace install all,用来下载所有插件,如果不可以,往下 ...
- Windows 进程的创建和终止
创建一个进程 总述 如图,创建一个进程主要分为两部分,用户态部分和内核部分. 既然我们想看看一个进程是怎么被创建的,那我们就用 WinDbg 来看看从用户态到内核态都调用了什么: 第一步:我们先看看 ...
- NC15665 maze
题目链接 题目 题目描述 小明来到一个由n x m个格子组成的迷宫,有些格子是陷阱,用'#'表示,小明进入陷阱就会死亡,'.'表示没有陷阱.小明所在的位置用'S'表示,目的地用'T'表示. 小明只能向 ...
- CF1703B ICPC Balloons 题解
题意:输入每个团队及团队的解决问题数,若是第一次解决则获得两个气球,其余获得一个气球. 做法:开一个数组记录是否为第一次解决该问题,直接模拟. #include<cstdio> #incl ...