事情要从一副蓝牙耳机说起。
我的耳机支持 multipoint,可以同时连 PC 和手机。平时 PC 优先出声,手机在 PC 空闲时接管播放。这套机制一直很稳,直到我打开 AliExpress 首页。
打开页面几秒钟后,手机音乐戛然而止;关掉标签页立刻恢复。静音标签页、静音 Firefox、静音 Windows 全都无效,页面上也看不到任何视频、音乐或媒体元素。这个现象可疑到值得追查。
排查:包装 AudioContext 构造函数
第一反应是自动播放的商品视频或广告。我逐一排查了常规嫌疑对象:
<audio>/<video>元素:没有HTMLMediaElement.play()调用:没有- 活跃的 Media Session 元数据:没有
navigator.mediaSession.playbackState:一直是none- 内嵌 iframe 里的媒体:没有
线索是问题并非立即出现,而是页面空闲几秒后才开始。我改用 Web Audio API 的思路去观察,在页面加载前先埋点,包装两个关键 API:
const OriginalAudioContext = window.AudioContext;
window.AudioContext = class extends OriginalAudioContext {
constructor(...args) {
super(...args);
console.log("AudioContext created", {
state: this.state,
stack: new Error().stack
});
}
};
// 同时包装 AudioNode.prototype.connect(),
// 看有没有节点连到 AudioContext.destination
这下抓到了:AliExpress 首页在空闲状态创建了两个 AudioContext,都进入 running 状态,都往 AudioContext.destination 上接了节点。而此刻页面上依旧是零个 <audio>/<video>、零次 play()、零条 Media Session,也听不到任何声音。
构造函数堆栈指向两个脚本:
https://assets.aliexpress-media.com/g/AWSC/uab/1.140.0/collina.js
https://assets.aliexpress-media.com/g/AWSC/fireyejs/1.231.67/fireyejs.js
两个脚本都挂在 AWSC 目录下,是阿里巴巴浏览器安全与反滥用(anti-abuse)体系的一部分,代码重度混淆。
隐藏的音频图:锯齿波 → 分析器 → 零增益
对混淆代码逆向后,两个脚本搭建的 WebAudio 图结构一致:
SawtoothOscillator(锯齿波振荡器)
→ AnalyserNode(分析器)
→ ScriptProcessorNode(脚本处理)
→ GainNode(增益设为 0)
→ AudioContext.destination
振荡器产生一个已知波形,分析器测量它经过浏览器音频实现之后的结果,脚本读取频率数据。增益设为 0,用户听不到声音;但整张图仍然连到了系统音频输出端。连上 destination 会让浏览器主动调度这张图做实时音频处理,即便最终音量是 0。
这跟自动播放视频有本质区别:没有 media 元素,浏览器标签页的静音控制就无从下手。对页面来说,它在执行实时音频处理;对我的耳机来说,这条音频通路被"占住"了,multipoint 自然无法干净地切回手机。
为什么"静音"仍然占着音频通道
这里有个容易被忽略的机制:WebAudio 的 GainNode 增益为 0,不等于图没在跑。
只要某个节点连接到了 destination,浏览器就会为这条链建立音频渲染调度。对底层来说,这意味着输出设备被打开、音频回调周期性地被驱动——对蓝牙 A2DP 而言,PC 这一侧的音频 sink 持续处于激活态。multipoint 耳机遵循"谁在出声(或谁的音频通路活跃)谁优先"的规则,于是 PC 牢牢抓住优先权,手机那头自然被顶掉。
标签页静音之所以失效,是因为它作用于 media 元素的音量/静音标志,而这里根本没有 media 元素。mediaSession.playbackState 是 none,因为图不是通过 media 元素播放的。这是一条绕开了浏览器所有可见媒体控制的"隐形音频通道"。
不止音频:一份完整的指纹清单
WebAudio 测试只是这套脚本的一小部分。翻看 bundle,还能找到大量测量代码,覆盖:
- Canvas 渲染与
toDataURL() - WebGL 渲染器信息、扩展、shader 精度
- 音频振荡器与分析器输出
- 屏幕与视口尺寸、devicePixelRatio
hardwareConcurrency与设备内存- 已安装浏览器插件
- 支持的音视频格式
- WebRTC 行为
- 浏览器性能计时
- 鼠标、触摸、焦点、滚动事件
- 设备运动与方向传感器
- 一系列与浏览器自动化(爬虫/Selenium 痕迹)相关的属性
结果会被序列化、加密,通过 fetch() 或 sendBeacon() 发往阿里的遥测服务。
音频指纹的原理是:同样的生成信号,在不同浏览器版本、操作系统、音频库、硬件上会得到略有差异的输出。单靠音频指纹未必能唯一定位一台设备,但把它和 Canvas、WebGL、硬件、时序、交互数据拼起来,区分度就非常高了。
动机:反欺诈与隐私的天平
从商业角度看,AliExpress 有充分理由区分真实用户和自动化客户端:账号接管、虚假账号、爬虫、自动下单、支付欺诈、刷评、滥用优惠券和新客补贴。Cookie 不可靠——可以被清除、复制、替换;而由十几个独立测量维度拼成的指纹,很难被一致性地伪造。交互数据还能辅助判断操作者是人还是脚本。
问题不在目的,而在实现方式。它在未触发任何敏感操作前就在普通购物首页跑起来,采集范围广、代码刻意不可读、且没有任何可见提示表明页面启动了一条实时音频处理链。更糟的是它产生了真实的硬件副作用——一条静默指纹测试能干扰蓝牙 multipoint 切换,而浏览器的静音控制对此无能为力。
防御:两条 uBlock Origin 过滤规则
实测屏蔽这两个脚本族后,AliExpress 首页照常渲染,捕获中不再出现 AudioContext 或 destination 连接。在 Firefox 的 uBlock Origin 仪表盘里,进"自定义静态过滤规则"加上:
! AliExpress AWSC fingerprinting scripts
||assets.aliexpress-media.com/g/AWSC/uab/*/collina.js$script,domain=aliexpress.com
||assets.aliexpress-media.com/g/AWSC/fireyejs/*/fireyejs.js$script,domain=aliexpress.com
点"应用更改"后务必关掉已有的 AliExpress 标签页再重新打开——屏蔽脚本并不会终止它已经创建的音频上下文。
这两条规则刻意收窄:只屏蔽观察到的两个脚本族,且仅在 aliexpress.com 域名下生效。因为它们连着反欺诈系统,屏蔽后登录或支付时可能出现更多 CAPTCHA;如果合法登录被拒,可以临时停用规则。目前首页和普通商品浏览不受影响。
如果你也想验证别的站点有没有类似行为,可以在 DevTools 控制台先跑上面的 AudioContext 包装代码,再打开目标页面看日志;state: "running" 且堆栈里出现可疑脚本,基本就是同一类东西。
一个开发者的视角
这件事最让我不舒服的,不是被采集数据,而是它让我无法掌控自己的音频通路。一个前端安全模块,通过一条零增益的音频图,就能改变我外部硬件的行为,而我唯一的线索是"手机音乐莫名停了"。这暴露的是浏览器给网页的权限边界和给用户的可见性之间的裂缝。
我自己的处理方式就两条:
- 默认拦:把上面两条规则常驻在 uBlock 里。反欺诈可以理解,但静默占用音频通路不能接受。
- 留个心眼:蓝牙耳机突然被"抢走"、手机播放异常中断时,先看看是不是有个看似安静的前台页面在搞小动作。包装
AudioContext那几行代码,随时能帮你把隐形音频图揪出来。
隐私和反欺诈从来不是非此即彼,但实现必须透明、克制。一个能让耳机"卡住"的静默指纹,无论如何都越界了。
浙公网安备 33010602011771号