一张自家子域上的图片,把主站所有人的登录态删得一干二净
本文目录
- 这行头跟别的响应头有什么根本不同?
- 四个取值分别管什么
- 为什么清Cookie会越过源的边界,扫到隔壁子域?
- 那么隔壁那个词呢
- Service Worker被注销意味着什么
- 同一行头,Chrome和Firefox为什么给出不同答案?
- __Host-前缀号称把源当安全边界,为什么挡不住?
- 除了退出登录那次跳转,还有什么地方会触发它?
- 一张自家子域的图片,怎么就把主站登录态删空了?
- 这种误配是怎么发生的
- 唯一那道开关为什么长在请求侧,而不是服务器上?
- 哪些写法会让整行头静默失效?
- 还有两道闸门在文档之外
- 你的站现在把这道边界外包给了多少台机器?
- 先说谁在用
- 再说暴露面
- 还有一层,搜索引擎完全看不到
- 一份可以直接跑的自查清单
- 常见问题解答
- 退出登录到底该不该用这行头?
- 我只在登出接口上配了这行头,还需要担心吗?
- 加了匿名跨域属性会不会影响资源加载?
- 为什么服务端日志里一点异常都看不到?
- 那行头把HTTP缓存也清了,对页面速度影响有多大?
- Service Worker被注销之后用户会立刻白屏吗?
- 这行头能当作安全加固项批量铺开吗?
- 权威参考资料
摘要:退出登录时用来清干净浏览器的那行响应头,同一行里逗号分隔的两个词,作用域不在一个层级上。
"cookies"清的是整个可注册域,会一路扫到你没碰过的兄弟子域;"storage"只待在发出这行头的那一个源里。更麻烦的是前者那个范围在两个主流浏览器上还不一样。实验台上最扎手的一条:页面停在主域没动,只加载了一张来自自家子域的图片,主域所有Cookie就被删空了,包括那条号称把源当安全边界的__Host-前缀Cookie。149个现网站点里,38%的首页挂着这样的兄弟子域,人均4.68个。
先说清楚这篇要拆的是什么东西。Clear-Site-Data是一行HTTP响应头,服务器发出去,浏览器收到之后把本地存的东西删掉。写法看着很朴素:
Clear-Site-Data: "cookies", "storage"大多数人第一次遇到它是在做退出登录,这件事历来比看上去麻烦,老论坛程序里退出登录之后连页面标题都会串掉的毛病就折腾过不少人。传统做法是服务端销毁会话、再回一串过期的Set-Cookie把浏览器里那几条盖掉,但盖不掉localStorage里的用户资料缓存、盖不掉IndexedDB里的草稿、更盖不掉Service Worker手里那份离线页。于是有了这行头,一句话把这些都交代了。
问题也在这儿。它是极少数具有回溯破坏力的响应头——别的头影响的是这一次请求怎么处理,这一行影响的是浏览器里已经躺了很久的东西。破坏性操作放在一行文本里,语法出一点差错就静默失效,作用域比字面看上去宽一大截,而且触发条件宽到一张图片就够。
下面这些结论不是读文档读出来的。保哥搭了一套双域实验台:主站、同一个可注册域下的另一台主机、外加一个完全不相干的域名做阴性对照,把这行头的每一个取值、每一种加载方式、两个浏览器内核都跑了一遍,服务端记日志,浏览器端读状态,两边对着核。
这行头跟别的响应头有什么根本不同?
差别只有一句话:别的响应头都是在给这一次交互定规矩,它是在下达一次删除指令。
Cache-Control告诉你这份内容能存多久,Content-Security-Policy告诉你这个页面能加载谁,X-Frame-Options告诉你这个页面能不能被嵌——它们的效果都作用在当下和之后。Clear-Site-Data作用在之前:用户三个月前存的东西,现在没了。
这个性质带来三个后果,后面每一节都在拆其中一个。
| 性质 | 带来的后果 | 在哪一节展开 |
|---|---|---|
| 删除是回溯的 | 没有灰度、没有回滚,发出去就执行完了 | 作用域那两节 |
| 执行在用户机器上 | 服务端日志一切正常,你看不见 | 触发路径那节 |
| 指令写在一行文本里 | 语法差一个字符就整条静默失效 | 失效写法那节 |
第二条跟CSP拦渲染不拦请求那次白屏排查遇到的困境是同一族:判决在别人机器上执行,你这边什么信号都收不到。区别在于那次是页面没渲染出来,这次是数据被删掉了,后者连“重试一次看看”的机会都没有。
四个取值分别管什么
规范定义了四个取值,加一个通配符。实测下来它们的分工是这样的:
| 取值 | 删掉什么 | 实测作用范围 |
|---|---|---|
"cookies" | Cookie,文档说还包括HTTP认证凭据 | 整个可注册域,跨子域 |
"storage" | localStorage、sessionStorage、IndexedDB、Cache Storage、Service Worker注册 | 只有发出这行头的那一个源 |
"cache" | 浏览器的HTTP缓存 | 发出这行头的那一个源 |
"executionContexts" | 本该重载所有正在展示这个源的页面 | 实测毫无动静,见后文 |
"*" | 以上全部 | 各自按各自的范围来 |
注意第一行和第二行的最后一列——它们不在一个层级上。这就是这篇文章要讲的主要一件事。
为什么清Cookie会越过源的边界,扫到隔壁子域?
实验台是这么搭的。主域记作P,同一个可注册域下的另一台主机记作Q,第三个完全不相干的域名记作R。三边各自种下一整套东西:一条只属于当前主机的Cookie、一条带Domain属性会跨子域的Cookie、一条会话Cookie、一条__Host-前缀Cookie,再加上localStorage、sessionStorage、IndexedDB、Cache Storage和一个注册好的Service Worker。
然后只在P上发一行Clear-Site-Data: "cookies",回头去数三边还剩什么。
| 位置 | 发头之前 | 发头之后 |
|---|---|---|
| P主域 | 4条Cookie齐全 | 一条不剩,服务端下次请求收到的Cookie头是空的 |
| Q兄弟主机 | 4条Cookie齐全 | 一条不剩,包括那条只属于Q的和那条__Host-前缀的 |
| R无关域名 | 4条Cookie齐全 | 纹丝不动 |
R这一列很重要,它是阴性对照。如果连R都被清了,那说明我的实验台漏了什么,整组数据作废。R稳稳当当,说明这个范围确实是有边界的——边界画在可注册域上,不是画在源上。
这不是浏览器擅自扩权。规范里写得清清楚楚,处理这个取值的第一步就是取“这个源的主机所对应的已注册域”,然后把Cookie存储里所有域属性能跟它匹配上的Cookie全捞出来删掉。MDN的措辞更直白,说这会影响整个已注册域包括子域,还专门举了个例子:在主域上发,测试子域的Cookie也一起清。
所以这行头里的"cookies",用的是Cookie自己那套按域名树走的边界,而不是浏览器里通用的那套同源边界。两套边界长得像,差别在实际部署里很致命——因为你的会话Cookie可能只在主域上有效,而删除令可以从域名树上任何一片叶子发出来。
那么隔壁那个词呢
把取值换成"storage",同一套实验重跑一遍,结果几乎是镜像的:
| 位置 | localStorage | IndexedDB | Cache Storage | Service Worker |
|---|---|---|---|---|
| P主域 | 清空 | 清空 | 清空 | 注册数1掉到0 |
| Q兄弟主机 | 原样 | 原样 | 原样 | 原样 |
| R无关域名 | 原样 | 原样 | 原样 | 原样 |
一步都没迈出去。"storage"严格锁在发出这行头的那一个源里,连自家兄弟主机都碰不到。
于是就有了这么一个局面:Clear-Site-Data: "cookies", "storage"这一行,你写的时候脑子里是“把这个站清干净”,浏览器执行的时候是两套尺子——左边那个词量的是整棵域名树,右边那个词量的是一个源。同一行、同一个逗号、同一次响应。
顺带一提,Cache Storage被清掉这件事,MDN那份取值清单里其实没列。文档写的是localStorage、sessionStorage、IndexedDB、Service Worker注册,外加几个早就退役的东西。实测它连Cache Storage一起端了。多清了总比少清好,但这说明那份清单不能当作完整依据来做风险评估。
Service Worker被注销意味着什么
这一条值得单拎出来,因为它的后果最不像“删了几个键值对”。
Service Worker一旦被注销,这个站的离线能力当场归零,之前预缓存的所有静态资源作废,下一次用户过来,全部回源重下。如果你的站是按PWA那套抓取与索引机制做的,那份精心设计的离线兜底页也一起没了。
更隐蔽的是它对性能指标的影响。回访用户本来是秒开的,现在跟首访一样重新拉一遍;如果同时还发了"cache",连HTTP缓存那一层也没了。核心网页指标的实地数据是按真实用户采的,这一下会实打实反映到分位数上,而你在服务端只会看到回源流量涨了一点,很难把它跟三天前那次登出改动联系起来。
同一行头,Chrome和Firefox为什么给出不同答案?
这是整轮实验里最扎手的一条,也是保哥没预料到的一条。
同一套实验台、同一行Clear-Site-Data: "cookies"、同一个发头的主机,换一个浏览器内核跑,Q那一列的结果反过来了:
在P上发"cookies" | Chromium内核 | Firefox |
|---|---|---|
| P自己的Cookie | 全清 | 全清 |
| Q上只属于Q的Cookie | 清掉了 | 保留 |
| Q上的__Host-前缀Cookie | 清掉了 | 保留 |
| Q上带Domain属性的Cookie | 清掉了 | 清掉了 |
| R无关域名 | 纹丝不动 | 纹丝不动 |
换成通配符"*",结论一样。
读一下这张表:Chromium按规范来,把整棵域名树上的Cookie都算成“这个站的”;Firefox只清了发头这台主机自己的,外加那条本来就声明了要跨子域的。两种做法各有各的道理——一个照着规范字面走,一个照着最小惊讶原则走——但对你来说,意味着“我在浏览器上验过了”这句话失去了意义。
这里有个很实际的坑。假设你做的是单点登录,主域一个后台、子域一个帮助中心,需求是“登出主域,帮助中心也跟着退”。你在其中一个浏览器上测,一次通过,上线。另一个浏览器的用户永远退不干净,而且不会有人报障——谁会为“我登出之后另一个系统还登着”去提工单呢,那看起来只像是没退干净的小毛病。
反过来那半边更糟:你的本意只是清自己这个源,结果在另一个浏览器上顺手把兄弟子域的登录态也删了。用户的感受是“我在A系统点了退出,B系统怎么也掉线了”,这种反馈通常会被归到“网络问题”里,很难查。
顺便说一句,这类跨浏览器差异跟前端平时熟悉的那种不一样。渲染差异、CSS前缀差异,肉眼可见、有工具可查、有兼容表可翻。数据清除层的差异既不在页面上,也不在技术栈检测扩展能看到的范围里,QA用例库里更不会有这一条。
__Host-前缀号称把源当安全边界,为什么挡不住?
上面那张表里有一行特别刺眼:__Host-前缀的Cookie也被隔壁主机清掉了。
这个前缀是干什么用的,先复习一下。带这个前缀的Cookie必须由HTTPS页面设置、必须带Secure、必须不带Domain属性、路径必须是根。满足这几条之后,MDN给它的评价是:这套组合让这条Cookie尽可能接近于把源当成一道安全边界,它只会发给设置它的那台主机,不会发给这个域上的任何其他主机。
换句话说,这个前缀的整个存在意义,就是防住“同一个域名树上的别的主机来动我这条Cookie”。而实测的结果是,在按可注册域清除的浏览器里,同一个域名树上的别的主机,一行头就把它删了。
这里面藏着一条容易被忽略的分工:__Host-前缀管的是谁能写、谁能读,从来没管过谁能删。它把写入权限锁死在一台主机上,删除权限压根不在它的设计范围里。
这个模式值得记住,因为它不止出现在这一处。凡是一个机制自称“安全边界”,都要分开问三遍:它管写吗,管读吗,管删吗。三个答案很可能不一样,而文档通常只会详细讲它管得最好的那一个。
补一个实测中的小插曲。Chromium在执行这行头的时候,会往控制台写一句回执,原话大意是已清除的数据类型是Cookie,同时明说清除信道标识与HTTP认证缓存目前尚不支持。而MDN关于这个取值的说明里写着它也会清除HTTP认证凭据。文档说清,实现说不支持——如果你的后台还有一层HTTP基本认证,别指望这行头能把它退掉。
除了退出登录那次跳转,还有什么地方会触发它?
这一节的结论是本文里最容易造成事故的一条:触发它根本不需要顶层导航。
规范里那句判定条件写得很朴素,大意是只要一个响应带着凭据标志,而且响应头里有这一行,就执行清除。注意,里面没有任何关于“这必须是一次页面跳转”的限定。
把这句话翻译成实验,跑出来的结果是这样的:
| 触发方式 | 这行头认不认 |
|---|---|
| 顶层导航(点退出,页面跳一下) | 认 |
| 一次AJAX请求的响应 | 认 |
| 一个script标签加载的脚本 | 认 |
| 一个img标签加载的图片 | 认 |
第二行就已经很值得注意了。现在做退出登录,很少还有人真跳一次页面,大多是发一个POST,成功之后前端自己跳路由。这个POST的响应头照样能触发清除——这其实是好事,说明现代写法不用为了这行头退回去做整页跳转。
后面两行才是问题。一张图片就能触发。
再往下追一层:如果这张图片来自另一个域,清的是谁的数据?实验做了对照,页面停在P,加载一张来自R(完全不相干的域)的图片,R的响应带这行头——结果是P分毫未动,R那边的Cookie被清空了。
也就是说,清除的对象是那个子资源自己所属的源,跟承载它的页面是谁没有关系。这条规则本身是合理的,谁的响应清谁的数据,符合直觉。但它跟上一节那个“按可注册域清”的规则一叠加,就叠出了一个相当不妙的东西。
一张自家子域的图片,怎么就把主站登录态删空了?
把两条规则并排放在一起看:
- 子资源的响应头算数,图片、脚本都能触发;
"cookies"的清除范围是整个可注册域,不是那一个源。
合起来就是:一张来自assets.example.com的图片,它的响应头里如果有Clear-Site-Data: "cookies",那么在按可注册域清除的浏览器里,www.example.com上的会话Cookie会被一起删掉。而这张图片可能就挂在你的首页上,每个访客都要加载。
这个推论必须验证,不能停在纸面上。实验是这样做的:浏览器停在P的页面上不动,只用一行脚本插入一张来自Q的图片,Q的响应里带那行头。然后回头查P还剩什么。
| 观察点 | Chromium内核 | Firefox |
|---|---|---|
| P页面里读到的Cookie | 空 | 四条齐全 |
| P的服务端下次收到的Cookie头 | 空 | 四条齐全 |
| Q自己的Cookie | 空 | 只剩那条跨子域的 |
左边那一列就是事故现场。用户什么都没点,页面都没跳,只是加载了一张图,服务端下一个请求收到的Cookie头是空的——会话没了,购物车没了,登录态没了。
右边那一列是这件事最难办的地方:在另一个浏览器上,这个事故复现不出来。你按用户描述去排查,本地怎么试都是好的。
这种误配是怎么发生的
没有人会故意在图片服务上发这行头。真实路径通常是这几种:
- 在网关或者CDN上写了一条全局响应头规则,本意只想覆盖某个登出接口,匹配范围写宽了;
- 子域上跑的是第三方SaaS,人家的后台有个“退出时清除本地数据”的开关,某位同事顺手打开了;
- 安全加固时按检查表批量加响应头,把这一行也当成“加了没坏处”的安全头一起铺开了。
第三种最常见,也最讲得通——这行头看着确实很像一个安全增强项。按层做响应头治理的时候,很容易把它跟HSTS、X-Frame-Options这类“配上就完事”的头归成一类。可那几类头是约束行为的,这一行是执行删除的,性质完全不同。
还有一层:nginx的add_header继承规则是当且仅当本级没写任何一条才向上继承,所以在某个location里随手加一条无关的头,可能把整层的头配置全摘掉,也可能把上层某条你以为只在别处生效的头带进来。反向代理那一层的头传递同理,改动人和受害路径经常不是同一个人在同一时间碰的。
唯一那道开关为什么长在请求侧,而不是服务器上?
好消息是这件事有得防。坏消息是防线不在你以为的地方。
回头看规范那句判定:只要一个响应带着凭据标志,而且响应头里有这一行,才执行清除。凭据标志是请求侧的属性,不是响应侧的。同一个URL、同一条响应头,用不同的方式去请求它,结果不一样。
把六种加载方式排开跑了一轮,控制台也一起录下来:
| 加载方式 | 结果 | 浏览器控制台说了什么 |
|---|---|---|
| img标签,默认 | Cookie被清空 | 已清除的数据类型:cookies |
img标签,加crossorigin="anonymous" | 纹丝不动 | 该请求的凭据模式禁止修改Cookie与其他本地数据 |
| script标签,默认 | Cookie被清空 | 已清除的数据类型:cookies |
script标签,加crossorigin="anonymous" | 纹丝不动 | 该请求的凭据模式禁止修改Cookie与其他本地数据 |
| fetch,credentials设为omit | 纹丝不动 | 该请求的凭据模式禁止修改Cookie与其他本地数据 |
| fetch,credentials设为include | Cookie被清空 | 已清除的数据类型:cookies |
一个HTML属性,两种相反的结果。这是本文唯一一条可以直接抄走的防御:对同一个可注册域下、由别人运维的那些主机,引用它们的静态资源时加上匿名跨域属性。
<img src="https://assets.example.com/hero.jpg" crossorigin="anonymous">
<script src="https://tags.example.com/loader.js" crossorigin="anonymous"></script>加了之后,那台主机的响应头就算带着删除指令,浏览器也会拒绝执行,并且在控制台留一句话说明为什么。顺带还有个好处:这些资源本来也不需要带上你的会话Cookie,不带反而更干净。
需要留神的是这个属性有副作用。加了之后请求会走跨域资源共享那一套,对方必须回Access-Control-Allow-Origin,否则资源加载失败。所以不能闭着眼睛全站铺开,得一个一个试。字体文件、需要读像素的画布图片本来就得带这个属性,那些不用改。
这条防线有个别扭的地方值得说破:出问题的是服务器上的一行配置,能补救的却是HTML里的一个属性。责任方和补救点不在同一个人手里——运维那边的头是第三方发的你改不了,前端这边加属性又需要知道有这回事。这大概是这类跨职能问题最典型的形状了,跟前端与SEO那几个协作动作点面对的是同一类断层。
哪些写法会让整行头静默失效?
这一节全是坑,而且每一个都不报错。
规范要求这行头的取值必须符合带引号字符串的语法。MDN的原话很硬:不带双引号的指令是无效的。实测把各种“看着只是风格差异”的写法都试了一遍:
| 写法 | 结果 |
|---|---|
Clear-Site-Data: "cookies" | 生效 |
| Clear-Site-Data: cookies | 整条失效,什么都没清 |
| Clear-Site-Data: 'cookies' | 整条失效 |
Clear-Site-Data: "COOKIES" | 整条失效 |
Clear-Site-Data: " cookies " | 整条失效 |
Clear-Site-Data: "cookies","storage" | 两个都生效 |
Clear-Site-Data: "wipeitall", "cookies" | 未知值被跳过,cookies照常生效 |
四种失效写法,共同点是全都不报错、服务端照样返回200、你在响应头里能看到这行字。只有真去数一遍浏览器里还剩什么,才知道它没干活。
最值得说的是第四行和第六行的对比。引号里边多两个空格,整条作废;逗号后边少一个空格,毫无影响。同一行文本里,有些空白位置无所谓,有一个位置致命,而且没有任何提示告诉你踩的是哪一种。
第三行也提醒了一件事:HTTP头的字段名大小写不敏感,这个字段的值大小写敏感。这两条规则挨在一起,很容易记串。
最后一行倒是符合预期,规范明确要求用户代理必须忽略解析时遇到的未知类型,为的是将来加新取值时不至于把老浏览器搞崩。这条设计是对的,但它也意味着你拼错一个取值名字,剩下的会照常执行,你还以为整行都生效了。
还有两道闸门在文档之外
第一道是安全上下文。规范里那句判定是:如果响应的地址不是一个先验可信的地址,就中断。翻译过来就是纯HTTP页面上这行头一个字都不算数。
这条做了单变量对照:同一个进程同时挂HTTP和HTTPS两个端口,主机名一样、响应字节一模一样,只有协议不同。
| 协议 | 页面自报的安全上下文 | 发"cookies", "storage"之后 |
|---|---|---|
| HTTPS | 是 | Cookie被清空 |
| HTTP | 否 | Cookie原封不动 |
HTTPS那一行是阳性锚点,它必须成立,这组数据才作数。两个浏览器内核跑出来一致。
这条的实际杀伤力在开发环境。本地起服务多半是HTTP,你写完退出登录逻辑在本地测,怎么试都没反应,很容易得出“这行头没用”的结论然后换个写法;或者反过来,本地测不出问题所以放行,上了HTTPS的正式环境才开始清。如果你的站还没全站切HTTPS,那迁移这件事又多了一条理由。
第二道闸门是"executionContexts"这个取值。规范说它的作用是让服务器要求客户端把当前正在渲染这个源的执行上下文全部废掉并重载。实测的结果是:
| 发出的取值 | 另一个标签页里的JS变量 |
|---|---|
"executionContexts" | 还在 |
"storage" | 还在 |
"cookies" | 还在 |
"*" | 还在 |
四组都没动静,包括通配符。查了一圈,三大浏览器引擎目前都没实现这个取值,有的把它列为实验性取值,有的在源码提交说明里直说除了它以外其他取值都实现了。
这个取值有一段公开的事故记录很值得读。有人在自己项目里把四个取值一起写上,升级浏览器版本之后用户会话被删了,控制台报的是无法识别的类型。当年那次的直接触发点就是这个还没实现的取值。这条记录挂在一个安全响应头库的问题列表里,到现在还能翻到。
结论很实际:别写这个取值。它不干活,还可能在某些版本上把整行头的解析带偏。你真想把已经打开的页面踢下线,老老实实靠页面自己轮询或者长连接推送。
你的站现在把这道边界外包给了多少台机器?
讲完机制,来看看现实里这件事的暴露面有多大。
保哥拉了149个能正常抓到的站,涵盖独立站品牌、几个主流电商平台上的店、支付服务商,还有一些非电商站做对照,统计了两件事:谁在发这行头,以及谁的首页挂着同一个可注册域下的别的主机。
先说谁在用
| 统计项 | 结果 |
|---|---|
| 首页响应头里有Clear-Site-Data | 0个 |
| 五条常见登出路径上有Clear-Site-Data | 0个 |
| 首页就注册Service Worker | 2个 |
| 页面里有manifest声明 | 20个 |
| 用了__Host-前缀Cookie | 0个 |
| 用了__Secure-前缀Cookie | 0个 |
这里得说明清楚采集口径:探测是在未登录状态下做的,只看了首页和五条最常见的登出路径。真正的登出接口往往要带着会话才会命中,所以这个0不能读成“全世界没人用”,只能读成“从站外用最常规的方式探不到”。
但即便打了这个折扣,这个0还是说明了点什么:这行头在这批站里不是一个日常配置项。这跟前面那个事故形态是相互印证的——正因为几乎没人主动配它,一旦哪台机器上冒出来一行,排查的人根本不会往这个方向想。
__Host-与__Secure-前缀双双挂零,跟前几轮普查的结论一致:这两个前缀的落地率长期趴在地板上。上一节讲的那个“防写不防删”的漏洞,对这批站来说压根还没排上号,因为根本没人用到那一步。
再说暴露面
这才是真正该看的数字。判定方式是把首页里所有引用到的地址拿出来,逐个用公共后缀列表算出可注册域,挑出那些跟主站同一个可注册域、但主机名不同的。
| 分组 | 站数 | 首页挂着兄弟主机的 | 占比 |
|---|---|---|---|
| 支付与结账服务商 | 23 | 19 | 82% |
| 非电商对照组 | 13 | 9 | 69% |
| WooCommerce店 | 9 | 4 | 44% |
| 国内跨境相关 | 11 | 4 | 36% |
| Magento店 | 14 | 4 | 28% |
| 独立站品牌 | 70 | 16 | 22% |
| Shopify店 | 9 | 1 | 11% |
| 合计 | 149 | 57 | 38% |
整体38%,这57个站合计挂着267个兄弟主机,人均4.68个。
更有意思的是这些主机都叫什么名字。出现频次最高的前缀依次是状态页、文档、帮助、支持、应用、隐私、开发者、服务端标签容器、社区、投资者关系、静态资源、博客。
把这份名单读一遍就知道问题在哪了:这些主机绝大多数不是你自己的服务器。状态页托管在监控服务商那儿,帮助中心是工单系统的,服务端标签容器是分析平台的,隐私页是合规工具的——都是一条CNAME指过去,别人的机器、别人的运维、别人的响应头配置。
于是那句话可以量化了:平均每个站把自己登出边界的完整性,外包给了4.68台自己碰不到的机器。任何一台上冒出一行这个头,在按可注册域清除的浏览器里,你的会话就没了。
Shopify那一档只有11%,倒不是因为他们更小心,而是那套体系默认把静态资源放在平台自己的域名下,跟商家域名不是同一个可注册域,天然就隔开了。这属于架构选择带来的意外收益。
还有一层,搜索引擎完全看不到
顺着SEO这条线补一句。Google的渲染服务是无状态的,官方文档明说本地存储与会话存储的数据在页面加载之间会被清除,HTTP Cookie同样在页面加载之间被清除。
这意味着这整套问题对爬虫是完全不可见的——它本来就每次都是干净的,你再清一遍它也感觉不到。抓取正常、索引正常、站长后台的告警一条不响。受影响的只有真实用户,而且症状是“莫名其妙被退出登录”这种最容易被归类成偶发故障的东西。
这也是为什么这类问题特别难被量化。它不进抓取日志,不进错误率,只会以一个很轻微的斜率出现在转化漏斗和回访率里。想把它抓出来,得按单因素隔离那套实验设计去做,成本比修它高得多。
一份可以直接跑的自查清单
- 把首页和几个关键页面的HTML拉下来,把所有引用的地址提出来,算一遍可注册域,列出跟主站同域但主机名不同的那些。
- 对着这份名单,逐台确认它是谁在运维,是自己的机器还是CNAME出去的第三方。
- 第三方那几台,逐个请求一次,看响应头里有没有这一行。有的话立刻在标签上加匿名跨域属性。
- 自己那几台,检查网关和CDN上有没有全局响应头规则会把这一行铺到静态路径上。
- 如果你确实要用这行头做登出,先想清楚要不要连兄弟子域一起清。要清,两个浏览器的行为不一样,得在服务端另做一份兜底。不要清,就别用这个取值,回到老办法逐条下发过期Cookie。
- 无论如何别写那个没人实现的取值。
- 敏感操作的验证一律放在服务端。这行头是清理手段,不是权限边界——它删的是浏览器里那份副本,服务端那份会话该失效还得你自己失效掉。
最后这条是最重要的。做后端这一侧的配合动作时经常会有一种错觉,觉得把客户端清干净了就等于退出登录了。清干净只是让这台设备上不留痕迹,会话本身在服务端是死是活,跟这行头没有一分钱关系。用PHP写登出的话,session_destroy()那一步一个都不能少,这行头是加在它后面的,不是用来替代它的。不同PHP版本下会话的默认行为有差异,这部分逻辑更要自己写死。
常见问题解答
退出登录到底该不该用这行头?
该用,但只用"storage"和"cache"这两个取值,Cookie还是老老实实用过期的Set-Cookie逐条下发。理由是"cookies"那个跨可注册域的范围在两个浏览器上表现不一致,你没法写出一份两边都对的实现;而"storage"严格锁在一个源里,行为可预测,正好补上传统做法清不掉localStorage和Service Worker的短板。
我只在登出接口上配了这行头,还需要担心吗?
需要担心的不是你配的那处,是别人配的那处。风险来自同一个可注册域下你管不着的那些主机——帮助中心、状态页、服务端标签容器。先把首页引用的地址过一遍,算出哪些跟主站同一个可注册域但主机名不同,再逐个查它们的响应头。
加了匿名跨域属性会不会影响资源加载?
会,加了之后请求走跨域资源共享流程,对方服务器必须返回允许来源的响应头,否则资源直接加载失败。所以要一个一个试,不能全站批量加。第三方SaaS提供的静态资源多半已经配好了跨域响应头,通过率不低,但必须实测。
为什么服务端日志里一点异常都看不到?
因为删除动作发生在用户的浏览器里,服务器只是发了一行文本。日志里能看到的只有一个正常的200,往后用户的请求会突然不带Cookie了,看起来跟一个新访客没有区别。唯一能提前发现的办法是在浏览器端主动做检测,比如登录态页面上校验关键Cookie是否还在,缺失时上报。
那行头把HTTP缓存也清了,对页面速度影响有多大?
实测"cache"确实清空浏览器的HTTP缓存,判据是服务端的真实命中数:清之前连取三次只回源一次,清之后再取三次又回源一次,而对照组发"cookies"时新增回源为零。影响的是回访用户,他们会退回到接近首访的加载成本。如果你的登出接口每天被调用的次数不少,这部分回源流量是能在监控上看出斜率的,做过回源率优化的人对这条曲线不会陌生。
Service Worker被注销之后用户会立刻白屏吗?
不会立刻。注销之后已经打开的页面通常还能撑到用户离开或刷新,真正的影响出现在下一次访问:离线兜底没了,预缓存的资源全部重新下载。所以现象往往滞后,你在改动当天什么都看不到,几天后才在性能指标上显出来。
这行头能当作安全加固项批量铺开吗?
不能,这是本文最想劝住的一件事。它跟那些“配上就完事”的安全响应头性质完全不同——那些是约束行为的,这一行是执行删除的,而且删除范围会越过源的边界。安全检查表上看到它就打勾加上去,是这类事故最常见的起因。
权威参考资料
本文标题:《一张自家子域上的图片,把主站所有人的登录态删得一干二净》
本文链接:https://zhangwenbao.com/clear-site-data-cookies-storage-scope-logout.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0