HTTP ETag头本是为缓存重新验证节省带宽而生——但一台为你分配唯一ETag的服务器,能把你的浏览器缓存变成追踪ID。
清除Cookie感觉像是按下了重置键,但它其实只清空了一个特定的罐子。ETag头的设计初衷十分平常——告诉浏览器"你已经有这个文件的最新版本了,不用重新下载"——而正是这个机制可以被挪作他用,悄悄给每位访客发一个独一无二、且会不断被静默确认的标识符,它活在浏览器缓存里,而不是Cookie存储里。用这种方式构建的缓存超级Cookie能挺过一次Cookie清除,因为它压根就从来不是Cookie。
核心要点
- ETag本来是用来标识缓存文件版本的,但没有任何规则阻止服务器把这个标识符做成每位访客唯一,而不是每个内容版本唯一——这就把普通的缓存重新验证变成了一次追踪往返。
- 这项技术是真实存在过的,而非纸上谈兵:Samy Kamkar的evercookie项目早在2010年就实现了一套可用的基于ETag的存储机制;2011年KISSmetrics被曝出利用ETag(配合Flash Cookie)复活了用户特意删除的ID,并因此引发集体诉讼。
- ETag并非唯一的缓存类追踪载体——HSTS标记、favicon缓存等任何网站能设置、之后又能读回的浏览器状态,都曾被用于同样的把戏;ETag只是记录最完整的一个案例。
- 现代浏览器已经堵上了这个漏洞的跨站版本。 自Chrome 86起,Chrome按顶级站点对HTTP缓存做了分区,此后在某个站点设置的缓存条目(及其ETag)就无法再被另一个站点读取——Safari和Firefox也各自上线了自己的分区方案。
- 同站的ETag追踪依然有效,因为分区机制只阻止跨站的缓存共享——单个站点仍然可以给你的浏览器分配一个独一无二的ETag,并在你每次回访这同一个站点时把它读回来,全程无需任何Cookie参与。
ETag到底是干什么用的
ETag是服务器附加在一个资源(图片、脚本、API响应等)上的HTTP响应头,作为版本标识符使用。MDN对这个头的描述很直白:它"让缓存效率更高、更省带宽,因为当内容没有变化时,Web服务器不需要重新发送完整的响应"。这个值具体怎么生成并没有统一标准;实践中通常是内容的哈希值、最后修改时间戳的哈希值,或者一个版本号。
重新验证的流程很简单:
- 你的浏览器请求一个资源,服务器返回内容以及一个
ETag: "abc123"头。 - 你的浏览器把内容和这个ETag值一起缓存下来。
- 下次请求同一个资源时,浏览器不会盲目重新下载,而是发送
If-None-Match: "abc123"。 - 如果服务器当前的ETag仍然匹配,它就回复一个不带内容的
304 Not Modified——带宽消耗降到最低;如果内容变了,就发送新版本以及新的ETag。
这个往返过程——浏览器证明自己已经有某样东西,服务器确认这样东西是否仍然是最新的——形态上和Cookie校验一模一样。唯一的区别只是标识符装在哪个头里。
ETag如何变成追踪标识符
HTTP规范里没有任何条款要求ETag必须代表内容版本。服务器完全可以在你第一次加载某个追踪像素或脚本时,生成一个唯一的、按访客区分的ETag,把这个值存在服务器端与你的会话绑定,再以ETag: "visitor-8f2c91a4"的形式发给你。你的浏览器没有办法分辨这是一个合法的缓存新鲜度令牌,还是一个追踪令牌,于是老老实实地把它缓存下来,并在此后每次请求这个资源时,通过If-None-Match把它原样回传——在没有任何Cookie、没有localStorage写入、甚至完全不需要JavaScript的情况下,悄悄地在每次页面加载时把你重新识别出来。
这正是Samy Kamkar的evercookie项目在2010年演示过的机制:除了HTML5存储、Flash本地共享对象等半打其他机制之外,它还包含一个由服务器端支撑的ETag组件,把一个标识符存进HTTP ETag,再从下一次请求中把它读回来——目的就是让这个ID在Cookie被清除之后依然能够复活。这个项目当初想证明的,正是一个铁了心的追踪者能拼凑出多少条独立于Cookie之外的存储路径,而ETag缓存最终被证明是其中相当耐用的一条,因为删除Cookie从来不会清空浏览器的HTTP缓存。
这项技术很快就从概念验证走向了实际部署:2011年,研究人员和记者证实,分析服务商KISSmetrics——服务对象包括Hulu和Spotify等网站——曾利用ETag配合Flash Cookie,让用户明确删除过的追踪标识符复生(respawning)。由此引发的隐私争议最终导致了一起集体诉讼和解,这也是ETag追踪被规模化部署、而非仅仅停留在概念验证阶段的最清晰的一个真实案例。
缓存类追踪不止ETag一种
ETag之所以最受关注,是因为它是最典型的例子,但它只是缓存类超级Cookie这个更大家族中的一员——任何网站既能设置、之后又能读回的浏览器状态,只要脱离了Cookie罐子,都可能被用来夹带标识符:
| 手法 | 设置了什么 | 之后读回什么 |
|---|---|---|
| ETag追踪 | 缓存资源上一个独一无二的ETag | 下次请求时的If-None-Match |
| HSTS超级Cookie | 一组子域名中,哪些被标记为HSTS强制HTTPS(每一位编码ID的一部分) | 浏览器是否在不发起往返请求的情况下,把每个子域名升级为HTTPS |
| Favicon缓存 | 一个带唯一URL、与其他存储分开缓存的favicon | 浏览器是重新请求它,还是直接使用缓存副本 |
| Cache-Control计时 | 某个资源是否存在于缓存中 | 缓存命中与重新抓取之间的加载耗时差异 |
这四种手法都和跳转追踪(bounce tracking)、CNAME伪装共享同一种结构性套路:它们利用的机制从一开始就不是为追踪设计的,所以那些专门针对Cookie的防御手段——拦截第三方Cookie、清空Cookie罐子——对它们根本不起作用。
为什么它能挺过Cookie清除和无痕模式
清除Cookie只会清空Cookie存储。HTTP缓存是一个独立的子系统,有自己的存储空间,在大多数浏览器的隐私设置里也有自己单独的"清除"复选框——而用户往往会跳过这个选项,因为"清除浏览数据"对话框会把缓存和Cookie列成两个互不相关的选项。写进缓存里的ETag会一直待在那里,不受影响,直到缓存本身被清除或该条目自然过期。
私密/无痕窗口本身也帮不上什么忙:一个私密会话在窗口打开期间同样会使用HTTP缓存,所以同一个私密会话早些时候设置的ETag,和普通窗口里的表现完全一样——它只有在窗口关闭时才会和其他一切一起被丢弃。我们关于网站如何检测无痕浏览模式的文章讨论的是一个相关但不同的问题——网站通过存储配额的怪异表现察觉到你正处于私密窗口,这和缓存类标识符能否在一个会话内部持续存在是两个完全不同的问题。
浏览器的应对:缓存分区堵上了跨站漏洞
最大的结构性修复其实并不是专门针对ETag——它瞄准的是HTTP缓存本身。从Chrome 86开始,Chrome按顶级站点对HTTP缓存做了分区:缓存资源不再只用URL作为键,而是用URL加上发起请求的顶级站点(在后续的改进版本中还加上了frame站点)一起作为键。Chrome团队自己的说明文章明确点出了这类滥用正是改动的动机之一——缓存的存在与否曾经被用来推断浏览历史,也曾被用来植入跨站追踪标识符,而分区机制消除了让这两种手法都得以成立的共享缓存。Safari和Firefox此后也各自上线了类似的缓存分区方案。
实际效果是:嵌在site-a.com上的追踪脚本,再也无法设置一个能在同一脚本加载到site-b.com时被读回的ETag,因为现在每个站点都拥有自己独立隔离的缓存分区。跨站ETag追踪——也就是曾让某家广告技术供应商能在自己嵌入的每一个网站上关联同一用户的那个版本——在当前主流浏览器里已经基本被堵死了。
分区机制没有解决的,是同站追踪:你反复访问的单个站点,依然可以在你第一次加载时分配一个独一无二的ETag,并在你每次回访时把它读回来,全程都发生在该站点自己的分区内,不涉及任何Cookie。缓存分区被设计出来是为了阻止跨站关联,而不是阻止一个站点用非Cookie的方式识别自己的回头客。
如何检测和缓解ETag追踪
不存在一个像"拦截第三方Cookie"那样一键就能消除这个问题的开关,这也正是为什么Privacy Badger自己那条关于检测ETag超级Cookie的issue(EFForg/privacybadger#2136)至今仍未关闭:合法服务器出于普通缓存目的会持续不断地设置ETag,所以从外部区分一个版本标识符和一个追踪标识符,是一个真正棘手的启发式问题,而不是简单的模式匹配。
有几件事确实有帮助:
- 连HTTP缓存一起清,别只清Cookie。 大多数浏览器的"清除浏览数据"对话框会把"缓存的图片和文件"列为独立于"Cookie及其他站点数据"的选项——两个都要勾上。
- 对不想被关联的会话使用私密/无痕模式,因为它的缓存(和Cookie一样)会在窗口关闭时被丢弃——只是它没法阻止追踪发生在同一个会话内部。
- 在敏感场景下彻底禁用缓存,可以通过浏览器开发者工具的"禁用缓存"选项,或者一款能剥离/随机化
ETag与If-None-Match处理逻辑的隐私扩展来实现——代价是失去ETag本应带来的带宽节省。 - 放心把跨站场景交给缓存分区去处理——它目前已经是各大主流浏览器的默认行为,所以性价比最高的那个修复,已经在你什么都不用做的情况下自动生效了。
这些方法都不需要读取页面上的JavaScript,因为这项技术的关键就在于它从不触碰任何脚本能检查到的存储API——它完全活在页面内容加载之前交换的HTTP头里。
ETag的交换本身发生在页面脚本读不到的那一层,但缓存标识符很少单独出手——追踪者会把它和你浏览器暴露的其他一切拼在一起。BrowserInsight的指纹检测会展示追踪者可能与缓存类ID组合使用的canvas、WebGL、存储等客户端信号,全部在你的浏览器里测量,绝不外传做任何分析。
常见问题
无痕/私密窗口能阻止ETag追踪吗?
只能在会话之间起作用,会话内部不行。私密窗口的缓存会在你关闭它时被丢弃,所以在那次会话中设置的ETag,没法在你重新打开浏览器后被读回——但只要这个私密窗口还开着,ETag式的重新识别就和普通窗口里表现得一模一样。
拦截第三方Cookie能阻止ETag超级Cookie吗?
不能,而这正是这项技术当年引起关注的全部原因。第三方Cookie拦截专门检查的是Cookie头;它对ETag或If-None-Match毫无感知,所以一个基于缓存的标识符可以毫无阻碍地穿过Cookie拦截器。真正能阻止跨站版本的,是HTTP缓存分区——一个完全独立的浏览器机制。
VPN或单纯清除Cookie能阻止这一切吗?
两者都不行。VPN改变的是你的IP地址,不是浏览器的缓存内容。清除Cookie清空的是Cookie存储,却完全不动HTTP缓存——以及躺在里面的任何ETag,而这恰恰就是这项技术专门用来利用的那道缝隙。
ETag追踪如今还在用吗?
自从Chrome、Safari和Firefox都上线了HTTP缓存分区之后,跨站ETag追踪的效果已经大打折扣——分区机制阻止了在一个站点设置的缓存条目(及其ETag)被另一个站点读取。同站ETag追踪——单个站点重新识别自己的回头客——在技术上依然可行,而且完全不在缓存分区的管辖范围之内。
结语
ETag追踪提醒我们,"清除你的Cookie"这句建议从始至终都只针对某一种特定的存储机制,而不是"不会再被识别"的保证。HTTP缓存的存在是为了让网络更快,而任何一种服务器既能写入、又能读回的机制——一个ETag、一个HSTS标记、一个favicon的URL——一旦被赋予唯一性而不是真正的内容版本含义,就随时可能被改造成一个标识符。缓存分区堵上了这个问题里危害最大的跨站版本;同站的这一半问题规模更小、动静也更小,但下次你清了Cookie却发现好像什么都没被重置的时候,它依然值得你多留个心眼。
推荐阅读:


