网页无法直接询问你是否装了广告拦截器,只能靠一个诱饵广告元素、一次被拦截的网络请求,或缺失的脚本全局变量来推断出结果。
网页没有办法直接问出“你是否装了广告拦截器?”这样的问题——它没有对应的浏览器 API,不能像查询你的时区或屏幕尺寸那样查询它。于是,想知道答案的网站不会去问,而是设下一个小陷阱,看它会发生什么。陷阱活下来,说明什么都没被拦截;陷阱没活下来,就说明有什么东西起了作用。
核心要点
- **网页是从结果反推广告拦截器的存在,而不是靠某个直接的信号。**三种常见方式是:一个用来测试过滤规则是否命中的诱饵元素、一个发往广告类地址却失败的网络请求,以及一个广告脚本本该定义、却始终没能定义的全局变量。
- **无论拦截器是传统扩展还是声明式规则引擎,同样的检测方式都能生效。**靠脚本工作的传统扩展,和 iOS 上的 WebKit 内容拦截器、Chrome 的
declarativeNetRequest这类声明式引擎,执行的是同一批过滤规则,因此在页面上留下的可观察痕迹是相同的。 - **一次检测本身只返回“有东西被干扰了”这一个比特,但哪些诱饵元素被命中,能把范围进一步收窄。**不同过滤列表针对的类名和网址模式各不相同,哪些诱饵存活、哪些没能存活,本身就是一个小小的指纹信号。
- **基于请求的检测,在完全没装广告拦截器时也可能误报。**DNS 级过滤、浏览器自带的跟踪保护或企业代理,都能造成一模一样的“广告没加载出来”——尽管它们都无法隐藏诱饵元素。
- **这篇文章讲的是检测原理,不是规避方法。**文中不包含任何隐藏拦截器、或破解反广告拦截弹窗的方法——那属于另一个对抗性的话题,本文不涉及。
每一次广告拦截器检测背后的三种方式
抛开各种变体,几乎所有网页上的广告拦截器检测,都可以归结为页面加载后不久运行的三种测试之一。
**诱饵元素。**网页创建一个 <div>(或类似元素),它的类名和结构与真实广告位一模一样——比如 ad-banner、pub_300x250、text-ad 这样的名字,尺寸和位置也和真实广告容器一样。网页把它插入页面,稍等片刻。广告拦截器主要依靠过滤列表(EasyList 及其同类列表)按选择器匹配元素,因此真正的拦截器会把这个诱饵连同任何匹配同一模式的真实广告一起隐藏或删除。随后网页检查诱饵是否还在:display 是不是被设成了 none、offsetHeight 是不是变成了零、节点是不是被彻底从 DOM 中移除。只要中招一项,就说明有东西拦截了它。BrowserInsight 自己的插件与扩展检测用的正是这种测试:插入一个位于屏幕外、带有 pub_300x250、text-ad、ad-banner 等类名的 div,约 100 毫秒后读取它的计算样式和布局尺寸。
注意,这是一种纯粹的外观层测试。诱饵是网页自己的脚本在本地创建的,全程不涉及网络请求——只有能在页面内部执行元素隐藏规则的东西,才能让它消失。
**被拦截的网络请求。**网页不去观察某个元素,而是发出一个看起来像广告或跟踪调用的请求——路径里带着 /ads/、主机名匹配某个已知的广告投放域名、脚本名字看起来像某个已知的分析标签。带有网络过滤规则(不只是外观隐藏)的拦截器,会在请求离开浏览器之前把它拦下,或者浏览器会把这次响应报告为失败。网页自己的脚本能看到这个失败——一次被拒绝的 fetch、一个触发了 onerror 而不是 onload 的图片——并把它当作信号。
**缺失的全局变量。**真正的广告投放脚本一旦成功加载,通常会在全局作用域里定义点什么——一个函数、一个配置对象、一个发布方自己的代码在继续之前会检查的标志位。如果广告脚本本身被拦截、没能加载,这个全局变量就永远不会被定义。网页会在脚本标签本该执行完的那一刻检查它是否存在;undefined 就意味着脚本从未运行过。
这三种方式的思路是一致的:先设定一个只有在没人干扰的情况下才会成立的预期,再检查它是否成立。它们都没有直接向浏览器提出一个问题,因为根本没有这样一个问题可问。
为什么它对每一种拦截器都同样有效
有人可能会觉得,浏览器扩展和内置的、没有任何扩展的拦截器,在检测者眼里应该完全不同。但实际上并非如此,原因在于两者是通过不同机制在执行同一套过滤规则。
传统的广告拦截扩展会注入内容脚本和样式表,隐藏或移除匹配的元素,也能直接拦截网络请求。声明式拦截引擎的底层做法不同,结果却殊途同归:浏览器事先拿到一份编译好的规则列表,由自己负责执行,不需要拦截器本身的代码逐页检查。WebKit 的内容拦截器就是这样工作的——它最早为 Safari 引入,如今是 iOS 上每一个内容拦截类 App 的基础:App 提交一份 JSON 规则列表,WebKit 就会在引擎层面把它应用到此后加载的每一个页面上。这些规则既能拦截资源,也能按 CSS 选择器隐藏元素(css-display-none 动作),所以网络类检测和诱饵检测它都能触发。Chrome 的扩展平台也在向类似方向演进:declarativeNetRequest 让扩展把静态或动态的规则集直接交给浏览器,而不是用 JavaScript 检查每一个请求;在 Manifest V3 下,这是 Chrome 要求网络拦截类扩展采用的模型。(MV3 拦截器隐藏元素时仍要靠注入 CSS,但背后用的是同一批过滤列表。)
机制不同——一边是由引擎执行的编译规则列表,一边是盯着页面的脚本——但网页能观察到的结果是一样的:匹配过滤规则的诱饵元素被隐藏或删除,匹配过滤规则的请求加载失败。一个围绕结果而不是机制搭建的检测器,不需要知道也不需要关心自己面对的是哪一种拦截器。这也是为什么一个“仅仅”是 Safari 内容拦截器、既不能运行脚本也读不到页面内容的移动端广告拦截器,依然会触发和桌面扩展一模一样的诱饵元素检测。
检测结果暴露了什么
单看一次这类检测的正向结果,几乎只是一个比特:是的,这个页面上有东西在过滤,或者不是。这一个比特对网站本身已经有用——它是触发反广告拦截弹窗、或者“请把我们加入白名单”横幅的开关——但单独来看,算不上什么强指纹信号,因为太多访客给出的答案都一样。
真正让它变得更精细的,是同时运行不止一个诱饵元素,每一个都按不同过滤列表的惯例设计。一个检测器同时放出五个诱饵 div——一个仿照 EasyList 的通用规则、一个仿照某个地区性列表的惯例、一个仿照只有更严格的“烦人元素”列表才有的规则——并分别检查每一个,得到的就不只是“被拦截:是”这一个结论了。它还能知道具体是哪些模式被命中,而不同用户订阅的过滤列表组合各不相同,这个具体组合就能收窄你所融入的人群范围。这和任何指纹信号背后的熵计算是同一回事:一个罕见的结果组合比一个常见的组合更具辨识度,我们关于指纹熵与匿名集的指南对通用信号讲的正是这个道理。广告拦截检测只是同一个思路下一个熵值很低的小例子——值几个比特,算不上单独的身份标识。
有必要说清楚这项技术不是什么。它和探测你到底装了哪些具体扩展不是一回事——后者靠的是探测某个扩展自己暴露的资源文件,或者它在页面上留下的独特副作用,是一种瞄准某个具名扩展的更精准的技术。广告拦截检测不需要知道是哪个产品在起作用,它只需要知道有什么东西干扰了它设下的诱饵。
误报:没装任何东西也可能被判定为“已拦截”
因为这种检测盯的是结果而不是一个直接的问题,任何能产生同样结果的东西都会触发它——而好几种和扩展毫无关系的常见配置恰好都会这样。不过要分清触发的是哪一种检测:作用在网络上的东西,能让“请求被拦截”和“全局变量缺失”两种检测报警,却碰不到网页在本地创建的诱饵元素。
- **网络层的广告拦截。**家里的 Pi-hole、带过滤功能的 DNS 解析器,或者路由器级别的广告拦截,会让广告请求根本到不了广告服务器——设备上没装任何扩展,广告脚本却照样加载失败、全局变量也不会出现,和被扩展拦截时一模一样。
- **浏览器自带的跟踪保护。**Firefox 的“增强型跟踪保护”默认会在隐私窗口中拦截已知的跟踪内容,切到“严格”模式后则对所有窗口生效,全程不需要任何附加组件。如果检测器加载的“广告”恰好位于它所用跟踪列表里的域名上,这次请求同样会失败。
- **自带广告拦截的浏览器。**Brave 的 Shields,以及 Opera、Vivaldi 等浏览器的内置拦截器,会自己执行过滤列表。视设置而定,其中可能包括元素隐藏规则——所以即便什么都没装,它们也可能触发诱饵元素检测。
- **企业或学校代理。**很多受管理的网络会把流量路由经过一个带过滤功能的代理,作为更广泛内容策略的副作用,顺带剥离掉广告和跟踪域名,这和用户自己安装了什么毫无关系。
- **能识破伪装跟踪器的 DNS 解析服务。**一些过滤型 DNS 服务更进一步,会顺着 CNAME 记录去识别借 CNAME 伪装的跟踪器——也就是藏在看似第一方子域名背后的第三方跟踪器。这样一来,即便某个广告或跟踪调用的网址看上去是无害的第一方地址,也可能被拦下。
这些情形都不涉及已安装的扩展,但每一个都能让网页得出“检测到广告拦截器”的结论。如果网站把这个信号当成已安装某个扩展的确凿证据,那就是在做数据本身并不支持的推断——它实际知道的,只是从页面到广告服务器之间的某个环节出现了干扰。
检测结果没能告诉网站什么
有必要把边界讲清楚,因为很容易高估一次诱饵检测的通过或失败究竟证明了什么。运行这项测试的网页并不会知道你到底装了哪个扩展(如果有的话)——那需要我们关于扩展隐私的指南里讲到的、更有针对性的扩展探测技术。它也不会知道你的身份或浏览历史。而且,正如上面的误报清单所示,仅凭一次失败的请求,它无法区分扩展、网络层过滤、浏览器的严格隐私设置,还是受管理网络的代理。它顶多能知道的,是这一次页面加载中,哪些可观察的模式受到了干扰——数量很有限的几个比特。
这篇文章讲的是这种推断是怎么构建出来的、它能证明什么、不能证明什么——不是一份规避检测的指南。过滤列表作者和反广告拦截厂商之间有一场持续的攻防,那不在本文讨论范围内;上文也没有任何隐藏拦截器、绕过反广告拦截弹窗的方法。
看看你自己的设置暴露了什么
BrowserInsight 的插件与扩展检测会在扩展检测的同时运行一个诱饵元素测试,你可以借此亲眼看看,网站在检测你的浏览器时到底能看到什么。由于这项测试属于外观层检测,Pi-hole 这类只在网络层工作的拦截器不会在这里显示出来。如果结果让你意外——明明没装扩展却显示被拦截,或者明明装了却没被拦截——可以看看是不是用了自带拦截器的浏览器,或者扩展的元素隐藏功能被关闭、当前网站被加入了白名单。想了解这类检测在完整浏览器指纹里处于什么位置,可以看看指纹检测,它展示了网页能观察到的关于你的设置的其余信息。
常见问题
网站能准确知道我用的是哪个广告拦截器吗?
不能,至少像诱饵元素或失败请求这样基于结果的检测做不到。这类测试只能确定有什么东西干扰了广告类内容或请求——它们无法识别具体是哪个扩展或哪种拦截机制在起作用。要指认某个具体扩展,需要一种针对该扩展自身痕迹的、更有针对性的技术。
这在手机上和电脑上的原理一样吗?
对于声明式拦截器来说是一样的。一个基于 WebKit 内容拦截器 API 构建的 iOS 内容拦截类 App,会产生和桌面浏览器扩展相同的可观察结果——诱饵元素被隐藏、广告请求失败——即便它根本没有能力在页面上运行脚本。检测器不需要知道具体是哪种机制在起作用。
我明明没装广告拦截器,也可能被判定为“已拦截”吗?
会的。网络层过滤(家用 DNS 拦截器、企业代理)和浏览器内置的跟踪保护,可能让“请求被拦截”或“全局变量缺失”类检测报警;而 Brave 这类自带广告拦截的浏览器,连诱饵元素检测也可能触发——这些情形都不需要安装任何扩展。
被检测到装了广告拦截器,隐私风险大吗?
单独来看是个很小的信号——比起一个指纹,更接近一个比特。只有当网站同时放出好几个样式各异的诱饵元素、并记录具体哪些被拦下时,它才会变得更有辨识度,因为这种命中模式在不同用户之间的差异,比单纯的“是/否”答案大得多。

