资讯动态

深入解析 puppeteer-extra-plugin-stealth 的 navigator.plugins 伪装:在 Headless 模式重建完整的插件与 MIME 类型环境

发布时间:2026/10/15 16:23:59 来源:尧图企业网站定制
网页爬虫浏览器控制测试【免费下载链接】puppeteer-extra Teach puppeteer new tricks through plugins.项目地址https://gitcode.com/gh_mirrors/pu/puppeteer-extra点击查看免费下载导读navigator.plugins与navigator.mimeTypes是浏览器指纹检测中最常被探测的一组 API真实用户使用 headful Chrome 时这两个属性包含 PDF 查看器、Native Client 等内置插件而 headless 模式下它们却是空数组这一差异足以让网站轻松识别自动化浏览器。本文以 puppeteer-extra-plugin-stealth 中的navigator.plugins反检测模块为线索结合仓库源码与测试用例完整剖析该模块如何从零构建一套功能完整、行为逼真且不留篡改痕迹的MimeTypeArray与PluginArray。读完本文你将掌握该反检测技术的实现原理、底层 JS Proxy 技巧以及如何在 puppeteer-extra 中单独或整体启用它。一、背景Headless 模式为何暴露空插件列表1.1 问题的本质根据 navigator.plugins 模块文档 的说明In headless modenavigator.mimeTypesandnavigator.pluginsare empty. This plugin emulates both of these with functional mocks to match regular headful Chrome.即在 headless 模式下navigator.mimeTypes和navigator.plugins均为空而正常的 headful Chrome 中它们包含可枚举的插件与 MIME 类型条目。这组差异是猫鼠游戏中极易被捕获的破绽检测脚本只需检查navigator.plugins.length是否为 0 即可判定浏览器是否为 headless——这一点在仓库的 cat-and-mouse.test.js 中作为检测手段出现await test(navigator.plugins empty, _ { return navigator.plugins.length 0 })1.2 为什么不能简单造个假数组仅仅把数据塞进一个普通 JavaScript 数组远远不够。检测方会通过Array.isArray()、JSON.stringify()、Object.getOwnPropertyNames()、Object.keys()、toString()、Symbol.toStringTag、函数toString()输出、原型链、错误堆栈乃至valueOf()等数十种手段交叉验证。因此该模块的目标是让伪造出来的对象在行为上无限逼近原生 headful Chrome 的对象而不是表面上有几个数据。二、模块 API 与使用方式2.1 核心类 API文档中暴露的核心类为Plugin它继承自PuppeteerExtraPlugin构造函数接受一个可选参数opts默认{}项目说明类名Plugin继承PuppeteerExtraPlugin构造参数opts可选默认{}插件名称stealth/evasions/navigator.plugins挂载时机onPageCreated通过evaluateOnNewDocument注入源码定义位于 index.jsPlugin类通过get name()声明插件标识并在onPageCreated(page)钩子中利用withUtils(page).evaluateOnNewDocument(...)将注入脚本注册到每个新页面的文档加载前。2.2 使用方式该模块是 stealth 插件默认启用的 16 个反检测模块之一见 stealth 插件源码 中的availableEvasions列表因此最常见的用法是直接整体启用 stealthconst puppeteer require(puppeteer-extra) const StealthPlugin require(puppeteer-extra-plugin-stealth) puppeteer.use(StealthPlugin()) // 默认启用全部 evasion含 navigator.plugins也可以绕过主模块像独立puppeteer-extra插件那样单独引入本模块参考 stealth readme 的说明const puppeteer require(puppeteer-extra) puppeteer.use( require(puppeteer-extra-plugin-stealth/evasions/navigator.plugins)() )甚至可以在整体启用 stealth 后动态剔除该模块const pluginStealth require(puppeteer-extra-plugin-stealth)() pluginStealth.enabledEvasions.delete(navigator.plugins) puppeteer.use(pluginStealth)三、实现原理从零构建可信的插件体系整个模块由五个文件协作完成index.js主入口、mimeTypes.js生成 MimeTypeArray、plugins.js生成 PluginArray、magicArray.js核心生成器、functionMocks.js函数级 Mock。3.1 入口流程onPageCreated的注入脚本按以下顺序执行对应 index.js头部检测若plugins in navigator navigator.plugins.length为真说明当前运行在 headful 环境立即返回不做任何改动避免重复污染。生成数组用data.json中的静态数据分别生成mimeTypes与plugins两个数组对象。交叉引用Plugin与MimeType互相引用enabledPlugin属性逐一建立双向关联。原型替换通过patchNavigator在Object.getPrototypeOf(navigator)上定义mimeTypes与plugins的 getter而不是直接给navigator加属性——这是防止Object.getOwnPropertyNames(navigator)泄露修改痕迹的关键。const patchNavigator (name, value) utils.replaceProperty(Object.getPrototypeOf(navigator), name, { get() { return value } })3.2 统一生成器 magicArrayMimeTypeArray与PluginArray结构高度相似因此 magicArray.js 用同一个generateMagicArray函数生成两者通过参数区分参数生成 MimeTypeArray 时生成 PluginArray 时protoMimeTypeArray.prototypePluginArray.prototypeitemProtoMimeType.prototypePlugin.prototypeitemMainProptypename生成过程包含四个层层递进的技术要点1非枚举属性描述符defineProp统一用Object.defineProperty设置writable: false, enumerable: false, configurable: true还原原生对象的属性描述符特征同时保证JSON.stringify(navigator.mimeTypes)不会输出内部数据。2原生原型造假对象每个条目通过Object.create(itemProto, descriptor)创建使navigator.mimeTypes[0].toString()返回[object MimeType]、Symbol.toStringTag为MimeType从而通过toString/原型链校验。其中Plugin条目还有一个特殊处理其length属性被硬编码为对应__mimeTypes的长度navigator.plugins[i].length应等于其关联的 MIME 类型数量且为非枚举。3Proxy ownKeys 陷阱原生MimeTypeArray/PluginArray的length属性虽然存在于原型上但 headful Chrome 执行Object.getOwnPropertyNames(navigator.mimeTypes)时却不会列出它。为复现这种魔法行为代码先给length打上configurable: true再用 Proxy 的ownKeys与getOwnPropertyDescriptor陷阱将其从属性列表中剥除同时伪造出基于数据的属性名列表索引 类型名如[0,1,2,3,application/pdf,...]。4函数级重定向Proxy 的get陷阱将item、namedItem以及 PluginArray 特有的refresh调用重定向到自定义 Mock其余属性正常透传。3.3 函数级 Mock复刻原生怪异行为functionMocks.js 负责item、namedItem、refresh三个方法的仿真item()复刻原生行为——参数会被尝试转换为整数并作为索引查找传入非整数包括字符串时返回第一个条目永远不返回undefined越界返回null无参数时抛出与原生一致的TypeErrorFailed to execute item ...: 1 argument required, but only 0 present.。namedItem()按itemMainProptype或name精确查找未命中返回null同样不是undefined无参数抛出TypeError。refresh()PluginArray 特有的方法直接返回undefined模拟原生什么都不做的语义。这些 Mock 通过utils.createProxy创建内部调用了 _utils/index.js 提供的stripProxyFromErrors剥离 Proxy 在错误堆栈中的痕迹与patchToString让函数toString()输出function item() { [native code] }。3.4 MIME 类型与插件的交叉引用文档明确指出mimeTypes and plugins cross-reference each other, so it makes sense to do them at the same time.这正是 index.js 中最精妙的部分遍历data.plugins把每个插件声明关联的 MIME 类型对象按索引挂到插件条目上plugins[name][index] mimeTypes[type]并定义非枚举的命名属性。给每个MimeType定义enabledPlugin属性指向对应插件。防循环引用enabledPlugin的值为new Proxy(plugins[name], {})——用空 Proxy 包装既保证访问语义正确如mimeTypes[application/pdf].enabledPlugin能取到插件又避免JSON.stringify(navigator.plugins)因循环引用而崩溃。唯一的例外是application/x-pnacl它与application/x-nacl共享同一个插件因此直接复用后者的enabledPlugin即同一个 Proxy避免产生两个不同的代理泄露不一致性。测试 mimeTypes.test.js 甚至专门验证了五层嵌套访问enabledPlugin[0].enabledPlugin[0]...suffixes仍能得到pdf证明该设计在深度遍历下也不会暴露循环引用问题。3.5 数据来源贴近真实 Chrome 的静态清单data.json 提供了模拟数据4 种 MIME 类型application/pdf、application/x-google-chrome-pdf、application/x-nacl、application/x-pnacl和 3 个插件Chrome PDF Plugin、Chrome PDF Viewer、Native Client。每个插件包含name、filename、description与__mimeTypes带双下划线前缀的内部字段在生成对象时被跳过。测试断言navigator.plugins.length 3、navigator.mimeTypes.length 4即基于此数据。四、为何能通过不留痕迹检测伪造对象不仅要长得像还必须查不出是假的。测试 index.test.js 验证了关键的一点启用插件后Object.getOwnPropertyDescriptor(navigator, mimeTypes)与plugins仍为undefinedObject.getOwnPropertyNames(navigator)中也不包含plugins。这得益于两点属性挂在原型上通过Object.getPrototypeOf(navigator)上的 getter 返回伪造对象navigator自身没有新增任何可枚举自有属性。全面的 Proxy 伪装ownKeys陷阱复现了原生属性列表stripProxyFromErrors清除了错误堆栈中的 Proxy 痕迹patchToString/redirectToString让所有方法输出[native code]配合utils.preloadCache预先缓存原生Function.toString与Reflect引用防止检测方通过替换这些基础函数来嗅探内部实现见 _utils/index.js。值得注意的是所有注入脚本都是通过 withUtils.js 的evaluateOnNewDocument以字符串形式传递stringifyFns/materializeFns因为page.evaluate的序列化机制不允许直接传递函数引用——这一约束也决定了整个模块必须采用函数字符串化 页面内 eval 还原的架构。五、测试验证一组可复现的行为快照该模块的测试用例index.test.js、mimeTypes.test.js、plugins.test.js用精确断言固化了伪装对象的行为基线以下是几组关键断言数组级行为来自 mimeTypes.test.js 与 plugins.test.js探测手段期望结果Array.isArray(navigator.mimeTypes)falseJSON.stringify(navigator.mimeTypes){0:{},1:{},2:{},3:{}}Object.getOwnPropertyNames(navigator.mimeTypes)[0,1,2,3,application/pdf,...]不含lengthnavigator.mimeTypes.toString()[object MimeTypeArray]navigator.mimeTypes.item()无参TypeError: Failed to execute item on MimeTypeArray...navigator.mimeTypes.item(madness)返回第一项application/pdf而非undefinedpush in navigator.mimeTypesfalse条目级行为来自 mimeTypes.test.js 的stealth: will have convincing mimeType entry探测手段期望结果navigator.mimeTypes[0].toString()[object MimeType]navigator.mimeTypes[0].__proto__[Symbol.toStringTag]MimeTypeJSON.stringify(navigator.mimeTypes[0]){}Object.getOwnPropertyNames(navigator.mimeTypes[0])[]enabledPlugin五层嵌套访问.suffixespdf无泄漏验证来自 index.test.js 的stealth: will not leak modifications启用插件后navigator自身的属性描述符与属性名列表与原生完全一致。此外cat-and-mouse.test.js 中stealth: will pass Paul Irish测试会在启用全部 stealth 插件含 navigator.plugins后运行 Paul Irish 的 headless 检测脚本其中就包含navigator.plugins.length 0这一项只有当所有检测项全部不命中时测试才通过。六、局限性与注意事项数据快照的时效性伪造内容来自 data.json 的静态快照Chrome PDF Plugin、Chrome PDF Viewer、Native Client与具体浏览器版本的实测数据可能略有出入检测方若对比多版本基线仍可能发现差异。跨 iframe 场景evaluateOnNewDocument注入不会自动作用于[srcdoc]/[sandbox]属性的 iframe相关说明见 cat-and-mouse.test.js这类隔离窗口中的navigator.plugins仍可能为空。无法做到绝对隐身仓库 stealth readme 也坦言完全阻止所有 headless 检测方式是不现实的本模块的目标是把检测成本提升到不划算的程度。结语navigator.plugins反检测模块是 puppeteer-extra stealth 系列中技术含量最高的实现之一它以data.json为数据源借助Object.create 属性描述符 双层 JS ProxyownKeys/getOwnPropertyDescriptor/get陷阱 函数级[native code]伪装构建出与 headful Chrome 行为高度一致的MimeTypeArray与PluginArray并通过原型链替换与错误堆栈清理做到查无实据。理解这套实现不仅能帮你用好 puppeteer-extra 的 stealth 能力也为自主编写反检测脚本提供了可复用的思路与工具_utils中大量工具函数均可独立复用。赞分享网页爬虫浏览器控制测试【免费下载链接】puppeteer-extra Teach puppeteer new tricks through plugins.项目地址https://gitcode.com/gh_mirrors/pu/puppeteer-extra点击查看免费下载相关推荐puppeteer-extra无头模式增强stealth插件如何模拟真实用户行为puppeteer extra无头模式增强stealth插件如何模拟真实用户行为 你是否曾遇到过这样的困境使用Puppeteer的无头模式Headless网页爬虫浏览器控制测试puppeteer-extra浏览器指纹伪装技术webgl.vendor与navigator插件应用puppeteer extra浏览器指纹伪装技术webgl.vendor与navigator插件应用 引言浏览器指纹追踪的隐形威胁 你是否曾疑惑为何刚浏览某网页爬虫浏览器控制测试puppeteer-extra-plugin-stealth 之 webgl.vendor 逃逸修复无头模式下 WebGL Vendor/Renderer 被标记为 Google 的指纹问题puppeteer extra plugin stealth 之 webgl.vendor 逃逸修复无头模式下 WebGL Vendor/Renderer 被网页爬虫浏览器控制测试创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑