HTTP响应头自相矛盾:142个站里108个在犯
本文目录
- 一份响应里同时写两条相反的指令,有多常见?
- 什么算矛盾,什么不算
- 十二种形态,按覆盖面排开
- 为什么这些东西一个都没被工具报出来
- 响应头本身有多大
- 两条并存,规范给的三种下场分别是什么?
- 第一种:合并成一条
- 第二种:其中一条自动作废
- 第三种:整条字段被丢掉
- 三种下场对应三种排查方法
- 同一个头发两次,浏览器会听哪一次?
- 五十多个站的表现一模一样
- 有一个站的两行说的是不同的事
- 三行cache-status暴露了整条链路
- 什么时候两行会真的出事
- 缓存那几个字段为什么最容易写岔?
- 不许存储和存一年,同时写在一行里
- 为什么大家爱这么写
- Expires写着1984年,Cache-Control写着现在
- segway那条最值得说
- 缓存字段该怎么写才不留隐患
- 拒绝嵌套这件事,为什么两套写法会打架?
- 这是少数几个跟搜索真有关系的安全字段
- 新版存在时,老版会被完全忽略
- delonghi把本地开发地址留在了线上
- 还有一批是把新语法写进了老字段
- 这一组的正确写法只有一句话
- 把新语法写进老字段,会发生什么?
- 浏览器不会猜你的意思
- 怎么发现这类问题
- 内容安全策略里也有同类陷阱
- 写字段前先去查一遍取值清单
- 内容安全策略里那82个白名单域,为什么一个都不生效?
- strict-dynamic的语义就是作废白名单
- 82个域名意味着什么
- 兼容性考虑是它的正当理由,但要写清楚
- 还有一种写法是纯粹的自相矛盾
- 只报告不拦截的那份策略,有多少是空转的?
- 在只报告模式里,有两条指令是被规范忽略的
- 更尴尬的是三个站连报告端点都没配
- 上一批量过一组类似的数字
- 只报告模式的正确用法
- 这些矛盾是谁写的,你还是平台?
- 两个占比40.1%的形态,都是平台指纹
- 被废弃的那条指令,站主删不掉
- 怎么快速判断一处矛盾是不是平台给的
- 自己写的那批,集中在哪几类
- 清掉这些矛盾,能省多少字节?
- 算一笔账
- 真正的代价是三样别的东西
- 那到底改不改
- 自查该按什么顺序做?
- 第一步:把最终响应完整拉一次
- 第二步:按字段分组,找同名重复
- 第三步:查三对经典冲突
- 第四步:把策略里的关键字逐个查一遍
- 第五步:实测那些静态查不出来的
- 第六步:把结果写成一张表,定期重跑
- 量这批数据的时候,哪几个判定差点搞错?
- 第一处:把合并当成了矛盾
- 第二处:分母必须是最终的2xx响应
- 第三处:判断策略指令时要区分两种模式
- 第四处:一个PHP的低级坑
- 为什么把这些写出来
- 常见问题解答
- 同一个响应头字段发两次,浏览器听哪一次?
- Cache-Control和Expires同时写,以哪个为准?
- X-Frame-Options和frame-ancestors都写了会怎样?
- 为什么我的X-Frame-Options写了一串网址却不生效?
- 策略里同时写strict-dynamic和白名单域名有意义吗?
- 只报告模式的策略要注意什么?
- 清理这些矛盾能提升性能吗?
- 权威参考资料
摘要:把142个海外品牌独立站首页的最终响应头逐字段拆开对照,108个站(76.1%)至少有一处自己跟自己打架的声明。并存不等于都生效,规范给的下场有三种:同名字段发两次会被合并成一条,两代指令并存会有一条自动作废,语法写错则整条字段直接被丢弃。最典型的一组是有5个站把内容安全策略的写法塞进了X-Frame-Options,浏览器不认这种取值,结果那行防嵌套声明等于没写。还有3个站在脚本策略里同时写了strict-dynamic和一长串白名单域,其中一个站列了82个域,按规范全部被忽略。
上一篇量的是robots.txt里两条规则撞车的事,判决依据是路径长度。有读者问了个好问题:同一份HTTP响应里两个字段说反话,是不是也有类似的裁决表?
有,而且比robots.txt那套复杂得多。robots.txt只有一条仲裁规则,HTTP这边每个字段各有各的合并逻辑,有的合并、有的作废、有的直接把整行丢掉。保哥把两周前抓的那批响应头翻出来重新过了一遍,专挑自相矛盾的地方看。
样本是142个最终返回200的站点首页,字段名去重233种。下面的数字都以这142个站为分母。
一份响应里同时写两条相反的指令,有多常见?
先给总数:108个站至少有一处,占76.1%。命中一类的38个站,命中两类的66个站,命中三类的4个站。也就是说四分之三的站点在自己的响应头里放了至少一处会被规范判掉的声明。
响应头能做的事情比多数人以为的多,X-Robots、缓存与Vary的实战机制是一份系统梳理。
上一篇量的是另一份文件里的同类问题,两条规则撞车时赢的不是先写的那条,判决依据是路径长度。
什么算矛盾,什么不算
判定之前得先划清界限。同一个响应里出现两个跟缓存有关的字段,本身不叫矛盾——Cache-Control和Expires可以共存,只要它们说的是同一件事。叫矛盾的是它们说的不是一件事:一个说别存,另一个给了个一年后的过期时间。
判据不清会让问题被夸大,大量无用页面拖垮流量的诊断与处置有一张决策矩阵。
声明与实际行为的落差我量过一次,说会变的有3个、真变的有17个就是那批数据。
缓存那几个字段的配合最容易写岔,怎么配才能既秒开又不出改了不更新的事故讲得比较细。
同理,X-Frame-Options和内容安全策略里的frame-ancestors可以共存,那是为了照顾老浏览器。但如果一个写着谁都不许嵌套、另一个允许自家子域嵌套,那就是两份互斥的意图挤在同一个响应里。
这个比例乍看吓人,实际要分层理解。里面有一大半来自平台默认下发的模板,站主既没写过也删不掉;真正由站主自己写出来的那部分,重算之后是51个站,占35.9%。后面会专门拆这一层。
十二种形态,按覆盖面排开
把142个站的响应头逐个拆完,一共归出十二种形态。前两种各占40.1%,是绝对的大头,剩下十种加起来才三成多。
形态分类清楚才好逐类处理,分面导航产生的海量URL怎么治理是同一种拆法。
服务器上那些配置项值得逐条核一遍,影响SEO的二十项服务器配置清单可以照着对。
同一批响应头里还量过另一件事,248个死字段里多数不是你写的而是平台统一下发的。
| 形态 | 站数 | 占比 | 规范给的下场 |
|---|---|---|---|
| 同名字段发了两次且取值不同 | 57 | 40.1% | 合并成一条 |
| 已废弃指令与取代它的新指令并存 | 57 | 40.1% | 老的那条不再有独立作用 |
| 同一个Cookie写了两套到期时间 | 22 | 15.5% | Max-Age赢,Expires被忽略 |
| 缓存指令内部打架 | 11 | 7.7% | 最严格的那条赢 |
| 防嵌套的新旧两版说的不一样 | 10 | 7.0% | 支持新版的浏览器只看新版 |
| Expires与max-age说的不是一个时间 | 8 | 5.6% | max-age赢 |
| X-Frame-Options写成了新语法 | 5 | 3.5% | 取值非法,整行丢弃 |
| 只报告模式里写了会被忽略的指令 | 4 | 2.8% | 该指令在这个模式下无效 |
| 白名单被strict-dynamic作废 | 3 | 2.1% | 整串域名被忽略 |
| preload写了但条件不满足 | 2 | 1.4% | 提交列表会被拒 |
| 跨域通配与凭据并存 | 2 | 1.4% | 浏览器拒绝整个请求 |
| 两代上报指令并存 | 1 | 0.7% | 新的那条赢 |
另一个边界是本文只看首页。响应头经常按路径规则下发,商品页、结算页、静态资源各有各的配置,首页干净不代表全站干净。选首页是因为它是唯一每个站都必然存在、且不需要猜地址的页面,代价是覆盖面窄。
为什么这些东西一个都没被工具报出来
市面上查安全响应头的在线工具有一堆,它们的检查逻辑基本是同一套:这个字段有没有、取值在不在推荐清单里、评个分。这套逻辑天然看不见矛盾——因为矛盾是两个字段之间的关系,而工具是逐个字段打分的。
看清一个地址的真实响应有顺手的办法,用接口测试工具查状态码与响应头比在线评分靠谱。
工具给的分数不等于事实,第三方数据到底准不准的六步校准法能挡掉大部分噪声。
更麻烦的是评分机制会奖励矛盾。你把X-Frame-Options和frame-ancestors都写上,两项都得分;写得对不对、有没有互相拆台,评分表里没有这一栏。于是照着评分工具做优化,做出来的往往正是这批并存声明。
还有一类容易被误判成矛盾的情况得排除掉:同一个字段在跳转链的不同段里取值不同。跳转响应和最终页面本来就该有不同的缓存策略,把它们混在一起统计只会得出一堆假问题。本文所有数字都只看最终那一段2xx响应。
响应头本身有多大
顺带量了一下体量。这142个站的响应头字段行数中位是27行,最少的4行,最多的42行;字节数中位3204,最小98,最大17951。矛盾字段本身占不了几个字节,所以清理它们的理由从来不是性能。
服务器上那些数值配置有反噬,限速拒掉的第四个请求正好是robots.txt会让抓取停十几个小时。
头部压缩在不同协议下差别很大,同样是4096的表HTTP/3早678字节就丢光了红利是实测出来的。
请求方向的字节账更值得看,同一份Cookie每次请求都重发一遍本来是可以只发一次的。
真正的代价是别的:一是审计时的假信号,二是它挤掉了本该配上的那些字段。一个站的安全响应头预算不是无限的,你在那儿写两遍互相拆台的防嵌套声明,就没人再去看跨域隔离那几项配没配。
两条并存,规范给的三种下场分别是什么?
这是本文最该记住的一节。并存的结果不是一种,是三种,而且它们的后果差别极大。
两个手段该用哪个也常被搞反,robots.txt和meta robots各管哪一段里分工写得很清楚。
状态码那一层也有类似的取舍表,301、302、404和410各自该在什么场合用能省掉不少争论。
第一种:合并成一条
HTTP的字段语法允许同一个字段名出现多次,语义上等价于把各行的值用逗号连起来。所以发两个Vary,一个写Accept、一个写accept-encoding,等价于发一个写着两者的Vary。这不是错误,只是写法不整洁。
字段堆太多还会撑爆请求,老用户打不开的页面而工具全都返回200就是这么来的。
这个字段写多了会把缓存切碎,决定收录哪一版的不是你的配置而是十分钟前路过的那个用户。
需要留意的是这条规则有例外。Set-Cookie就不能这样合并,它的取值里本来就带逗号,所以规范专门给它开了口子——每一行是一个独立的Cookie。这也是为什么Set-Cookie永远是响应头里行数最多的那一块。
第二种:其中一条自动作废
这是最容易出事的一类,因为两条声明都合法、都会被解析,只是有一条按规范被无视了。缓存那一组最典型:同时给了Cache-Control的max-age和Expires,规范明确要求以max-age为准,Expires只在没有max-age时才被看。
多层缓存下的取舍更复杂,回源、TTL分层与缓存键怎么配有一份完整实战。
验证类字段配错的代价也不小,没改过的页面对爬虫重发了几百遍是同一类浪费。
Cookie上也有一模一样的一对:同时写Expires和Max-Age,Max-Age赢。样本里22个站的Cookie两样都写了,多半是为了兼容极老的浏览器,代价是那个Expires值成了摆设——如果两者算出来的时间不同,你以为的过期时间就是错的。
顺带说个容易混的点:作废和丢弃不是一回事。作废是这条声明被解析了、也被理解了,只是按规范让位给另一条;丢弃是它根本没被当成有效声明。前者你至少知道行为是可预期的,后者等于凭空少了一个字段。
第三种:整条字段被丢掉
最狠的一种。字段取值不符合语法,浏览器不会去猜你的意思,直接当这行不存在。X-Frame-Options只认两个取值,写别的就属于非法取值;内容安全策略里某个指令写了不认识的关键字,那条指令会被整个跳过。
有些字节级问题症状反而很明显,一个看不见的字节头就能让页面白屏是另一种极端。
同样的静默失败在结构化数据里更狠,一个尾逗号就能让整页标记失效而页面照常显示。
这一类的可怕之处在于它没有任何反馈。控制台不一定报,工具不一定查,你只能通过实际行为发现——而防嵌套这种东西平时根本不会有人去试。
这三种下场还有一个共同点:全都不产生任何错误提示。页面照常打开,控制台通常也是安静的,只有在你真的去试那个被声明约束的行为时才会显形。这跟语法错误完全不同——语法错误至少有人会告诉你。
三种下场对应三种排查方法
合并类只需要整理,不影响行为,优先级最低。作废类要查两条声明说的是不是同一件事,不一致就以规范判赢的那条为准,把另一条改成一致或者删掉。丢弃类必须实测,因为静态看是看不出来的。
常规全站审计有覆盖边界,桌面爬虫能查出的十二类问题清单可以对照看缺哪块。
资深团队栽跟头往往在结构性问题上,被忽略的那几类技术SEO失灵原因说的就是这种局面。
一堆问题先修哪个得有依据,五百个站实测排出来的技术SEO优先级可以当排期底稿。
我给客户做检查时的顺序就是倒过来的:先查丢弃类,再查作废类,最后才顺手整理合并类。花的时间也差不多是这个比例,丢弃类要一条条实际发请求验证。
同一个头发两次,浏览器会听哪一次?
57个站命中了这一类,是并列第一的形态。展开看会发现它几乎全是同一个来源。
配置每次reload都通过不代表没事,Googlebot每八次抓取就有一次撞在301上是同一类隐形代价。
取响应头的姿势不对会漏字段,用HEAD查说没配缓存头、换成GET那五个全在是踩过的坑。
五十多个站的表现一模一样
这57个站里有绝大多数是同一种写法:两行Vary,一行写着Accept,另一行写着accept-encoding。连大小写风格都一致——前一行首字母大写,后一行全小写。这种一致性只能来自同一套平台代码。
平台决定的东西不止响应头,架构搭错了爬虫根本找不到商品页是更上游的问题。
平台默认给的东西要先认清,Shopify的128种结构化数据类型怎么选也是同一个问题。
选平台时就该问清楚默认配置,托管、自建还是纯代码怎么选把各自代价摊开了。
按规范这两行合并成Vary: Accept, accept-encoding,行为完全正确,没有任何损失。它只是把一件本可以一行说完的事说了两遍,而且两遍风格还不统一。
有一个站的两行说的是不同的事
真正值得看的是那些不属于这套模板的。bershka.com发了两行Cache-Control:一行写着no-cache、no-store、must-revalidate,另一行只写no-cache、no-store。合并之后等价于三个指令都在,那个must-revalidate不会因为第二行没提就消失。
在边缘层改东西越来越常见,在CDN边缘改SEO的原理与落地形态给了几种现成形态。
多层架构下每层都可能插一手,多层缓存如何同时左右体验指标与抓取讲了各层的分工。
这种情况通常说明响应经过了两层:应用层写了一版,前面某个代理或者边缘节点又加了一版,两边都没检查对方写过没有。合并结果碰巧是安全的,但下次某一层改成了相反的指令,问题就出来了。
三行cache-status暴露了整条链路
另外两个站发了三行cache-status,分别来自平台的持久缓存层、框架层和边缘层,每一行报告自己那一层的命中情况。这种写法是规范鼓励的——它本来就设计成多层各写一行。
缓存层数一多就得逐层看,索引器、缓存与Redis调优怎么讲透是一个完整案例。
把链路上的记录收拢起来才好排查,日志怎么收集成可检索的结构是越早做越好的事。
顺带说,这也是一个很好的排查素材。三行摆在一起,能一眼看出请求穿过了几层缓存、每层是命中还是回源。可惜大部分站只有一层缓存,或者干脆不发这个字段。
什么时候两行会真的出事
合并规则的前提是这个字段的语法允许逗号分隔的列表。对不允许列表值的字段,两行就是灾难——比如Content-Length发两次且值不同,规范要求当成错误处理,中间设备可能直接断开连接。
压缩协商配错同样会静默失分,出厂只压HTML一种类型、其余字节全额计入预算是常见默认值。
协议层的错误常常悄无声息,抓取速率被调低的那几天日志里一条错误都没有就是这种情形。
样本里没有出现这种情况,因为这类错误会立刻导致页面打不开,早就被发现了。真正长期存活下来的矛盾,都是那些不影响页面正常显示的。
缓存那几个字段为什么最容易写岔?
缓存这一块有两组独立的矛盾:一组发生在Cache-Control内部,一组发生在它和Expires之间。前者11个站,后者8个站。
这些字段通常写在同一个配置文件里,重写、缓存、规范化与HSTS六层治理可以一起看。
同一批域名上量过验证类字段,142个站里只有52个回得出304口径卡得很死。
不许存储和存一年,同时写在一行里
Cache-Control的取值是一串用逗号隔开的指令,它们之间没有语法上的互斥要求,所以什么组合都写得出来。样本里最常见的组合是private加no-store加no-cache加max-age=0,四个指令堆在一起,10个站是这个写法。
压缩与缓存经常被混为一谈,免插件压缩HTML与GZIP叠加怎么做分清了两件事。
缓存判断写错的表现很隐蔽,首页一直是旧数据、一个字符改对缓存判断就是这类问题。
这一串里真正起作用的只有no-store,它的语义是任何缓存都不得存储这个响应,其它三个说的事情它全包了。private是说共享缓存别存、私有缓存可以存,跟no-store直接冲突;no-cache是说可以存但每次要回源验证,同样被no-store覆盖。
反过来看,真正需要精细控制的站会怎么写?样本里做得最干净的几个,首页只写一条Cache-Control,取值是private加max-age=0,意思是浏览器自己可以缓存但每次都得验证,共享缓存别碰。一条说清楚一件事,别的什么都不加。
为什么大家爱这么写
这串东西是有历史来源的。不同年代的浏览器对缓存指令的支持程度不一样,早年确实需要把几个指令都写上才能保证行为一致。这套写法被写进了各种教程和框架默认值,一路抄到了今天。
框架默认值抄进来的东西不少,头部那几行dns-prefetch和emoji代码怎么去掉是同一种清理。
这类失分往往第一年就埋下了,建站第一年最容易失分的十二项配置是同批经验的汇总。
抄来的配置攒久了就是欠账,技术债怎么排查和分批偿还给了可执行的顺序。
今天它不会造成实际损失,因为最严格的那条会赢,结果是安全的。但它掩盖了一个问题:写这行的人未必知道自己想要哪一种行为。等到哪天需要把首页改成可以被边缘缓存几秒钟,他会不知道该删哪几个词。
Expires写着1984年,Cache-Control写着现在
另一组更有意思。8个站的Expires和Cache-Control说的不是一个时间,其中casetify.com的Expires写着1984年1月11日——那是Netscape时代用来表示立即过期的经典写法,比很多读者的年纪都大。
时间戳的零点经常被当成占位值,从sitemap的lastmod到结构化数据的日期都得转对。
时间格式这一层坑不少,ISO 8601、sitemap与HTTP头各自要什么格式有一份速查。
otto.de写的是1970年1月1日,也就是时间戳的零点。还有几个站直接写了一个减一。这些写法在纯Expires时代都是有效的立即过期表达,今天它们和同一响应里的max-age=0并存,按规范以max-age为准,Expires被完全忽略。
| 站点 | Cache-Control | Expires | 谁生效 |
|---|---|---|---|
| casetify.com | max-age=0, no-cache, no-store, must-revalidate | 1984年1月11日 | max-age=0 |
| otto.de | private, no-cache, no-store, max-age=0 | 1970年1月1日 | no-store |
| segway.com | no-store, no-cache, must-revalidate, max-age=0 | 三十天后 | no-store |
| bolia.com | no-cache, no-store | 减一 | no-cache/no-store |
segway那条最值得说
前面几个站的Expires写的是过去时间,跟max-age=0意图一致,属于写法冗余。segway.com不一样:它的Expires写的是三十天之后,而Cache-Control写着不许存储。两个字段的意图完全相反。
老技术还在链路上跑这件事很常见,那套跳转脚本下线之后留下的迁移账是个完整复盘。
老设备与新协议共存是常态,整站都开了HTTP/3而最关键那个请求还走HTTP/2就是例子。
链路上还有谁在读你的响应,AI爬虫抓取量已经超过Googlebot好几倍改变了很多判断。
按规范当然是no-store赢,浏览器不会存。但如果链路上有一个只认Expires的老代理,它就会把这个页面存三十天。这种设备在企业内网和某些运营商网络里还真不少见。
顺便说个判断技巧:看到一串四五个缓存指令堆在一起,先问写它的人想要哪一种行为。答不上来就说明这串是抄来的,可以整段换成一条明确的指令。答得上来的话,让他把那句话写成注释放在配置旁边,比什么审计都管用。
缓存字段该怎么写才不留隐患
我的建议很简单:只写Cache-Control,不写Expires。后者存在的唯一理由是兼容HTTP/1.0,而今天的链路上纯1.0设备已经稀有到可以忽略。多写一个字段不但不能提高兼容性,反而制造了一个需要长期保持同步的副本。
换架构之后这套东西得重搭,sitemap、重定向这些不会自动跟过来是常见上线失分点。
这类改动需要后端配合,后端工程师配合SEO的七个动作点是从真实账本里总结的。
要是因为某些历史原因必须保留Expires,那就让它和max-age算出来的时间严格一致,并且把这件事写进部署检查里。人工维护两个必须同步的值,早晚会漂。
拒绝嵌套这件事,为什么两套写法会打架?
防止别的站把你的页面套进iframe,历史上有两套写法:老的X-Frame-Options,和内容安全策略里的frame-ancestors。10个站的两套写法说的不是一回事。
内容被别人拿去用是同一类风险,一稿多发怎么不被副本反超给了归属声明的做法。
页面被别的东西顶替的后果很严重,人机验证屏被当成正文索引、规范网址判给了别站是真实事故。
这是少数几个跟搜索真有关系的安全字段
Google那边对安全响应头的态度一直很清楚:绝大多数跟搜索没关系。唯一被点名说有关系的就是防嵌套这一类——因为别人把你的内容嵌进他的页面,可能会让那一页跟你抢排名。
整套加固该做哪些项,权限分离、目录迁移与应急响应的生产级清单可以对照排期。
安全字段里另一个值得配的是强制加密,HSTS怎么配、preload提交流程与回滚路径有完整步骤。
所以这一组值得单独花时间。它既是安全配置,又是少数几个能直接影响可见度的响应头。
新版存在时,老版会被完全忽略
规范写得很直白:支持frame-ancestors的浏览器必须忽略X-Frame-Options。不是取交集,不是取更严格的,是完全忽略。这一点和缓存那边的最严格者胜完全不同,很多人会记混。
两种手段能不能一起用是高频疑问,noindex和canonical同时用的九种场景逐个给了答案。
有些声明本来就只是提示不是命令,canonical是提示不是指令的八种误用解释了为什么它常常不生效。
于是就有了样本里最刺眼的两个例子。dji.com的X-Frame-Options写着deny,也就是谁都不许嵌,而同一个响应的frame-ancestors写着自己所有子域都可以嵌。现代浏览器只看后者,那行deny等于没写。gillette.com是同样的组合。
delonghi把本地开发地址留在了线上
更有戏剧性的是delonghi.com:X-Frame-Options写着deny,frame-ancestors写的是自己加上localhost的任意端口。localhost那一项显然是开发阶段为了调试加的,上线时没删。
流程走形通常从第二周开始,那批页面早已进了索引说的就是没人复查的后果。
开发环境的东西带到线上代价可能很大,测试站被索引后的八步清除与四层防御是完整方案。
它实际造成的风险很低,因为攻击者的localhost指向的是他自己的机器。但这一行说明了一件事:这份策略从开发环境一路带到线上,中间没有任何人重新读过它。
这类问题在多品牌集团里尤其常见。同一套响应头模板被复制到几个站点,其中某个站点的开发环境配置连同调试用的来源一起被带到了线上,而复制它的人根本不知道那一项是干什么的。样本里那对同集团品牌的P3字段取值一模一样,就是同一种复制的产物。
还有一批是把新语法写进了老字段
5个站的X-Frame-Options里塞了一串完整的网址。onepeloton.com写的是sameorigin后面跟着四个具体地址,其中一个还带着明显的临时部署域名。anker.com、eufy.com、soundcore.com、insta360.com也是类似写法。
装太多东西之后冲突排查更难,应用栈精简与冲突排查给了可执行顺序。
模板重复带来的冲突很常见,插件和主题各冒出一套canonical怎么归一是同类清理。
这是把frame-ancestors的语法照搬到了X-Frame-Options上。可后者只认两个取值,别的一律算非法。非法取值的处理是整行丢弃,所以这五个站的这行声明一个字都没生效——它们同时还有frame-ancestors,所以实际防护没塌,但那一行纯属白写。
还有个细节值得留意:这五个站里有四个属于同一个消费电子集团旗下的品牌矩阵。同一份写错的模板被复制到了四个站上,而每个站的运维大概都以为这是总部审过的标准配置。模板复制这件事会把一个人的疏忽放大成一批站的问题。
这一组的正确写法只有一句话
写frame-ancestors,把X-Frame-Options保持成最简单的deny或sameorigin,或者干脆不写。两者的意图必须一致,且老字段不要试图表达新字段才有的能力。
判断一个站的技术欠账有多深,三类站点的高ROI修复清单可以当快速评估表。
完整的诊断框架能避免漏项,从抓取、内容到AI可见度的审计框架可以当检查表。
需要允许某几个具体来源嵌套时,只能靠frame-ancestors。这时候老字段该写什么?写sameorigin是最稳的——支持新版的浏览器会忽略它,不支持的会退回到最保守的行为。
把新语法写进老字段,会发生什么?
上一节最后那个现象值得单独展开,因为它是三种下场里最隐蔽的一种:整条字段被丢掉,而且没有任何提示。
写在哪里也会决定生不生效,工具说在head、浏览器说在body该信哪边是一次真实排查。
有些地址连声明的地方都没有,接口和feed拿什么说自己不想被收录是同一类结构性缺口。
浏览器不会猜你的意思
HTTP字段的解析规则是严格的。取值不符合语法,处理方式是当这个字段不存在,而不是尽力理解。这跟HTML的容错解析完全相反——HTML里标签写错了浏览器会想办法补救,HTTP头里写错了就是没有。
解析与渲染各步都可能丢东西,抓取和渲染DOM分几步、哪一步会丢讲得比较完整。
容错解析这件事在HTML那边完全不同,语义化HTML到底影响不影响抓取拿样本页跑过一遍。
这个差异经常被搞混。写前端的人习惯了浏览器帮忙纠错,写响应头时会下意识以为差不多就行。实际上这一层没有差不多。
还有一个信号值得留意:如果一个字段的取值里出现了另一个字段才有的语法元素——分号分隔的指令、带引号的关键字、完整网址——那多半就是搬错了地方。这个特征用一条正则就能扫出来,适合放进上线前的检查脚本。
怎么发现这类问题
静态检查基本无效,因为字段确实在响应里,工具能看到它。唯一可靠的办法是实测:真的构造一个嵌套页面试一下,或者打开开发者工具看控制台有没有关于该字段的警告。
页面被什么挡在索引外可以一次查清,可索引性体检把几层原因分开列比逐个猜快得多。
页面骨架层面也有一次性体检,揪出标题层级、图片alt与语义标签短板适合改版后跑。
Chrome对非法的X-Frame-Options取值会在控制台留一条警告,写得还挺清楚。问题是没人会为了检查一个响应头专门去开控制台看,而这条警告只在真的发生嵌套尝试时才出现。
内容安全策略里也有同类陷阱
一条指令里写了浏览器不认识的关键字,那个关键字会被忽略;如果整条指令的语法坏了,整条指令被跳过,但同一份策略里的别的指令照常生效。所以一份策略可能有一半在工作、一半是死的,而从外面看它是完整的。
第三方悄悄改默认值我量过一次,默认配置变更却没人通知的漂移比例并不低。
白名单漂移是这套策略的老毛病,白名单上明明写着那个域名、浏览器还是拦了是一次完整排查。
这也是为什么严格的内容安全策略必须配报告端点。没有报告,你永远不知道哪几条在真的拦东西、哪几条从来没被触发过。
这里还有一层容易被忽略的风险:同一个字段在不同规范版本里的合法取值可能变过。曾经存在过的allow-from写法早就被各家浏览器移除了,但它仍然出现在不少年代久远的教程里。照着老教程写出来的配置,语法上像模像样,实际是一行被丢弃的死字段。
写字段前先去查一遍取值清单
说起来朴素,但这类问题的成因就是没查。X-Frame-Options的合法取值只有两个,写第三个就是错;Referrer-Policy的合法取值有八个,写错一个整行失效。这些清单都在文档第一屏,花不了三分钟。
各家工具读同一个页面结论未必一致,十款技术栈检测扩展的实测对比能看出差异有多大。
生成器给的预设也未必跟规范对齐,通配符判定会和标准打架的那几种情形值得先看一眼。
更值得建立的习惯是:任何一个响应头字段改动上线后,用命令行拉一次真实响应,把改动的那一行原样贴出来看。人眼看一遍能抓住绝大多数手误,比任何工具都快。
内容安全策略里那82个白名单域,为什么一个都不生效?
这是本批最戏剧性的一处。3个站在脚本策略里同时写了strict-dynamic和一长串白名单域名,其中bollandbranch.com列了82个,burrow.com列了38个,italic.com列了31个。
默认加载的东西本身就该定期清,关掉用不上的那几个默认脚本是同一种瘦身。
跨域相关的字段还会影响监控,页面上近一半资源在监控里是一排零就是被这类字段挡的。
strict-dynamic的语义就是作废白名单
这个关键字的设计目的是解决白名单模式的老问题:白名单越列越长、越长越不安全。它的规则是一旦出现,同一条指令里所有基于地址的来源全部被忽略,只有带正确nonce或哈希的脚本才被信任,而且这些脚本动态加载的其它脚本会自动继承信任。
边缘防护的效果没那么确定,防火墙到底拦不拦得住AI爬虫实测答案挺意外。
分层防护的思路在别处也一样,robots、UA识别、WAF三层的选型框架比只改一处靠得住。
所以strict-dynamic和白名单不是叠加关系,是替代关系。写了前者,后面那一串域名在支持它的浏览器里一个字都不看。
把这三个站的白名单长度排开看也有意思:82、38、31。这个量级说明它们都到了白名单模式的极限——域名多到已经无法评估风险,正是引入strict-dynamic的典型动机。所以它们其实是走在正确路上的团队,只是没把上一代的东西收干净。
82个域名意味着什么
那82个域名是一份完整的第三方脚本清单:分析工具、A/B测试、客服插件、支付、推荐引擎、广告像素。整理这份清单大概花了不少时间,每一个域名背后都对应着一次沟通和一次上线。
这些脚本背后是一整套数据链路,把各渠道花费拉进来算统一ROAS说明了它们为什么必须留着。
每加一个第三方都是一次决策,怎么给店铺装上行为分析工具是其中最常见的一类。
而它们全部被同一行里的一个关键字作废了。这不是安全事故——strict-dynamic本身更安全——但那份清单的维护成本白花了,更要命的是维护它的人以为自己在做一件有意义的事。
兼容性考虑是它的正当理由,但要写清楚
公平地说,同时写两者有一个正当理由:不支持strict-dynamic的老浏览器会退回到白名单模式。规范也是这么设计的,属于渐进增强。
把约定写成文字永远划算,达标定义、违约责任与四类经典纠纷是另一个场景的同一道理。
共同维护一份规则文件要立规矩,共享与个人怎么分、规则冲突听谁的是一套可搬用的做法。
问题在于这三个站的白名单里都没有注释说明这是兼容路径,团队里新来的人看到那一长串域名,只会以为它是当前生效的规则,继续往里加。做兼容退路可以,但必须在部署脚本或文档里写明白,否则它就会变成一份没人知道是死的活清单。
还有一种写法是纯粹的自相矛盾
另一个组合更直接:同一条指令里既写了nonce又写了unsafe-inline。规范规定有nonce或哈希时,unsafe-inline被忽略。这个组合同样有兼容意图,但风险方向相反——它让人误以为内联脚本还能跑,上线后发现某个内联片段被拦了,排查半天。
优先级这类提示有明确的生效条件,浏览器资源优先级的机制与实战讲清了边界。
资源提示写了未必被读,Googlebot为什么不读你的preload说明了哪些提示其实无效。
这次样本里这个组合是0个站,10个用了nonce的站里没有一个同时写unsafe-inline,说明用nonce的团队通常懂得比较深。倒是24个站直接写了unsafe-inline而没有nonce,那属于另一个话题了。
只报告不拦截的那份策略,有多少是空转的?
内容安全策略有一个只报告不执行的模式,用来在正式上线前观察会拦掉什么。样本里5个站发了这个模式的策略,其中4个站有问题。
监控闭环怎么搭是通用问题,四步把引用率监控做成闭环可以套到别的指标上。
写了却没人读这件事我专门量过,有多少条指令压根没有接收方比例比想象中高。
在只报告模式里,有两条指令是被规范忽略的
frame-ancestors和sandbox这两条,在只报告模式下不生效,规范明文写了。原因也合理:这两条的效果是阻止加载或改变文档的沙箱状态,没法只报告不执行。
验证一条建议有没有效有个笨办法,给那条建议配一个注定无效的对照组比讲道理管用。
观察模式在别处也有讲究,A/B测试怎么做才不影响SEO里有官方表态和收尾动作。
byredo.com、govee.com、rab.equipment、vaude.com都在只报告策略里写了frame-ancestors。写的人多半想的是先观察一下有谁在嵌我的页面,可惜这条恰恰是观察不到的。
更尴尬的是三个站连报告端点都没配
只报告模式的全部价值在于收到报告。byredo.com、rab.equipment、vaude.com三个站的策略里既没有report-uri也没有report-to,响应里也没有对应的端点定义字段。
收上来的数据要能读懂才有用,读懂Googlebot抓取与预算浪费给了分析路径。
没有记录就没有结论,AI爬虫到底有没有抓你的站那套挖法可以直接套用。
这意味着这份策略既不拦截,也没有任何人收得到它产生的报告。它在响应里占着几百个字节,纯粹是一份自我安慰。
上一批量过一组类似的数字
这个现象和上一批量的网络错误上报是同一类。当时的结果是64个站启用了错误上报,全部只挂在旧版的端点定义上,而新版定义只有2个站在用,两者交叉之后没有一个站配对上。声明与接收端分离,是响应头里的常态。
同一批样本上还量过分组,给单个爬虫开组会丢掉通配组全部规则中招比例高得离谱。
名单上那些名字有没有真来过也能量,266个爬虫令牌里只有23.3%真的来过是另一组实测。
区别在于那一批是新旧版本没接上,这一批是压根没配。前者还能说是迁移没跟上,后者只能说是抄了一半。
只报告模式的正确用法
先配端点,再写策略,最后才观察。顺序反了就是空转。观察期建议至少两周,覆盖一次完整的营销活动周期——很多第三方脚本只在活动期间才加载。
能交给机器的就别靠人记,哪些SEO工作能交给工具、哪些不能划了一条边界。
观察期该覆盖一次完整活动周期,大促与日常SEO的八维度差异给了完整时间轴。
观察完之后要做的事是把只报告改成正式执行,而不是让它一直挂着。样本里那5个站没办法判断挂了多久,但从策略内容的陈旧程度看,至少不是这个月才加的。
这些矛盾是谁写的,你还是平台?
统计到一半就能看出来,这批矛盾里有相当大一块不是站主写的。判断依据很简单:完全相同的写法在几十个毫不相干的品牌上同时出现。
托管环境替你做的决定不止一处,主机可能正悄悄拦AI爬虫而监控没报警是同一种失控。
平台层面的例外规则也要留意,通配组的星号对广告爬虫不生效是最容易漏的一条。
两个占比40.1%的形态,都是平台指纹
57个站发两行Vary,57个站在策略里同时写升级不安全请求和阻止混合内容。这两组的站点名单高度重合,而且这批站在上一批的响应头体检里也是同一群——它们共用同一个托管电商平台。
批量生成的地址最容易被忽略,博客标签页怎么避免权重稀释给了处理方法。
平台生成的东西要单独处理,集合页没有产品时该怎么办有三种场景的分别处置。
把这两类从总数里刨掉,剩下自己写出来的矛盾还有多少?重算之后是51个站,占35.9%。这个数字更接近站主自己该负责的部分。
这里得说句公道话:平台下发这些字段本身不算错。它要照顾几十万家店铺、各种年代的浏览器,选一份保守的通用配置是合理的工程决策。问题出在它不告诉店主自己下发了什么,也不提供关掉某一项的开关。
被废弃的那条指令,站主删不掉
阻止混合内容这条指令早就被升级不安全请求取代了,前者的行为是拦截,后者是自动改写成安全连接。两者并存时,后者先起作用,前者基本没有机会触发。
平台还在往响应链路上加新东西,要不要向AI爬虫按次抓取收钱已经有平台在做了。
该不该留下一个已经没用的东西,FAQ富结果被砍之后那段标记还写不写是同一道选择题。
这条是平台在响应头模板里统一下发的,店主既没写过它,也没有权限删。上一批量响应头里那批没有接收端的字段时,遇到的是完全一样的局面:站主连清理的权限都没有。
还有个更省事的判断:看这个字段在你自己的配置文件里搜不搜得到。搜得到就是自己写的,搜不到还出现在响应里,那它一定是链路上某一层加的——可能是平台,也可能是边缘防护或者反向代理。这三层各自会加什么,值得单独整理一份清单。
怎么快速判断一处矛盾是不是平台给的
三个办法。第一,把那一行原样贴进搜索引擎,看看有没有一堆别的站出现同款;第二,看这个字段的取值风格跟你自己写的那些是不是一致,平台下发的通常大小写和空格风格与众不同;第三,直接换一个同平台的店铺地址抓一次响应头,一比就知道。
另一套代理的行为也值得对照,mod_proxy全景与HTTPS配置跟前者的差异不小。
反向代理那一层经常会加字段,proxy_pass的斜杠、重写与WebSocket全场景配置说明了它能改什么。
确认是平台的之后,能做的事情不多,但至少可以从自己的待办清单里划掉,同时在文档里记一笔,免得下次审计又被同一条报出来。
还有一个判断维度是看这条声明有没有随业务变过。平台下发的字段在所有店铺上完全一致、多年不变;自己写的那些会带着业务痕迹,比如某个促销活动留下的临时来源、某次合规整改加上的策略。带痕迹的那些才是你真正需要维护的。
自己写的那批,集中在哪几类
刨掉平台指纹之后,剩下的分布是:Cookie双到期22个站、缓存内部打架11个站、防嵌套两版不一致10个站、Expires与max-age不一致8个站、老字段写新语法5个站。
结构化数据里也有大量白写的字段,112个独立站里17个在犯的商家名称错误是一份对照。
有些声明必须两层都写才算数,一键退订少写一层就整个域名限流是代价最直接的一例。
这五类有个共同点,它们都发生在需要人做判断的地方。平台可以替你下发一份通用策略,但你的Cookie存多久、首页能不能被缓存、允许谁嵌你的页面,这些只能自己定——而定的时候没人查规范。
清掉这些矛盾,能省多少字节?
先把这个问题堵死,免得有人拿性能当理由去说服老板。
有一条硬边界可以本地复现,Googlebot那个2MB抓取上限怎么测有现成的检查器。
字节预算这件事在页面层更要命,46个电商站里4个的商品链接根本没被读到是实测结果。
算一笔账
响应头字节数中位是3204,最大的那个站17951。矛盾字段本身,一行Vary大概二十来个字节,一行Expires三十几个,X-Frame-Options五十上下。全清理掉,一个典型站点能省一百多个字节。
把字节换算成成本是另一个视角,从页面碳足迹到爬虫抓取的同一套规范给了完整算法。
性能到底怎么影响排名得先说清,页面速度与核心指标的优化优先级免得把力气花错地方。
一百多个字节在一次页面加载里连零头都算不上。上一批算过一个类似的数字:那批无接收端的字段合计8145字节,占响应头总字节的1.74%。拿性能当理由,站不住脚。
还有一个不太被提起的代价是招人与交接。一个新同事接手这套配置,第一件事是读懂它;读到两条互相拆台的声明时,他要么去查规范,要么就照着现状继续加。前者花时间,后者让问题继续长大。
真正的代价是三样别的东西
第一是审计噪声。每次做安全检查,这些矛盾都会以某种形式冒出来,要么被工具报成问题,要么被人看到之后花时间确认,年复一年。
推不动改动往往不是技术问题,甲方拒绝建议八成源于身份冲突给了重写汇报的办法。
注意力分配本身就是决策,内容、技术、外链三类分工与团队配比是同一个协作问题。
第二是误判风险。你以为防嵌套配好了,实际上生效的是另一条;你以为Cookie三十天后过期,实际按另一个值算。这类误判只有在出事那天才会暴露。
第三是它挤占了注意力。一个团队能分给响应头的时间是固定的,用来维护两份互相拆台的声明,就没时间去看跨域隔离、权限策略这些真正还缺的东西——样本里权限策略的覆盖率只有4.2%。
顺带提醒一句:改响应头这件事的回滚成本远比看上去高。它通常写在服务器配置或者边缘规则里,改一次要走完整的发布流程,而且影响面是全站。所以别为了清理整洁性问题单独发一次版,攒到下次动那块配置时一起做。
那到底改不改
我的判断是分三档。丢弃类必须改,因为那是真的没生效;作废类看情况,如果两条声明的意图一致就留着不管,不一致就必须统一;合并类不用管,等下次动那块配置时顺手整理。
判断一样东西该不该留有通用问法,用第一性原理做工具选型说的就是删掉会不会出事。
最终由谁说了算有明确逻辑,Google选择规范网址的九条决策逻辑可以照着排查。
唯一需要立刻动手的是那五个把新语法写进老字段的站,以及那三个只报告却收不到报告的站。前者是配置没生效,后者是白占字节。
自查该按什么顺序做?
把这套检查落到自己站上,顺序是固定的,因为前一步能筛掉后面大部分工作。
开发期就该埋好这些点,自建站开发阶段的十大优化要点能省掉上线后的返工。
从零起步的话顺序更重要,前十二周从技术地基到内容蓝图是一份排好序的清单。
第一步:把最终响应完整拉一次
注意是最终响应,不是第一次响应。多数站首页会经过一到两次跳转,中间那几跳的响应头跟最终页面的完全不同。用命令行工具跟随跳转,把最后一段单独存下来。
跳转链本身也要单独查,两个方向的301跳转怎么配有完整实战。
跟随跳转这件事各家实现不同,工具报的死链和Googlebot抓的从来不是同一批就是这么来的。
还有一个坑是只发HEAD请求。有些服务器对HEAD和GET返回的字段不一样,上一批就遇到过用HEAD查出来说没配缓存头、换成GET之后那五个字段全在的情况。一律用GET,把响应体丢掉就行。
第二步:按字段分组,找同名重复
把所有字段名列出来做一次计数,大于一的挑出来。排除掉Set-Cookie、Link这些本来就允许多行的,剩下的就是候选。看它们的取值一不一致,不一致的记下来。
整理文本这件事别靠肉眼,把JSON-LD调试这件事讲透省下不少比对。
结构层面的一次性体检也有工具,一次扒清内外链结构与扣分项适合改版后做。
这一步能顺手发现链路结构:同一个字段被写了两次,通常说明请求穿过了两个都会写这个字段的层。知道有几层,后面排查会快很多。
这三对之外还有一对值得顺手看:跨域相关的那两个字段。允许来源写成通配符、同时又允许携带凭据,浏览器会直接拒绝整个跨域请求。样本里2个站是这个组合,属于配置了等于没配置的典型。
第三步:查三对经典冲突
缓存那一对、防嵌套那一对、Cookie到期那一对。这三对覆盖了样本里绝大多数自己写出来的矛盾,检查逻辑也简单,各自只需要比对两个值说的是不是一件事。
统计侧的字段配置同样要对,默认追踪不到的那部分流量怎么补需要过滤器加渠道分组。
Cookie这一层还有合规要求,评论区那个Cookie提示怎么汉化并默认勾选是个小而全的例子。
检查时注意规则不一样:缓存和Cookie是最严格者或者新指令胜,防嵌套是新版存在则老版被完全忽略。三对三种规则,别混着记。
第四步:把策略里的关键字逐个查一遍
内容安全策略是最容易堆出矛盾的字段,因为它是一整套语言。重点查三处:有没有strict-dynamic和白名单并存、有没有nonce和unsafe-inline并存、有没有在只报告模式里写那两条会被忽略的指令。
信号之间打架时怎么定夺,六类消歧信号的管控实战给了处理顺序。
多份声明怎么合成一个实体,@graph与知识图谱怎么搭是另一套合并规则。
查完之后如果发现有兼容意图的组合,别删,在部署脚本里加一行注释说明这是给老浏览器留的退路。注释是给下一个人看的,而下一个人才是真正的风险来源。
实测的时候记得区分浏览器。不同浏览器对非法取值的处理宽严不一,有的会尽力解析出一个合理值,有的直接丢弃。以最严格的那个为准来定结论,因为你的用户里总会有用那个浏览器的。
第五步:实测那些静态查不出来的
丢弃类只能靠实测。做一个最小的测试页,用iframe嵌一次自己的页面,看能不能嵌进去;打开控制台看有没有关于字段取值非法的警告。整个过程五分钟,但它是唯一能验出整行被丢掉的办法。
浏览器还会悄悄改你的页面,自动翻译把yes改成forks而数据看不出异常是同一种沉默污染。
这类小脚本自己写最快,用Vibe Coding做SEO小工具的八步避坑讲的就是自养工具。
如果站点有多个模板类型,商品页、列表页、内容页各测一次。响应头经常是按路径规则下发的,首页配对了不代表商品页也对。
做这张表还有个附带好处:它能让你在跟平台方沟通时有据可依。指着表里那一行说这个字段是你们下发的、我们删不掉、它和我们的配置冲突,比泛泛地说安全头有问题有效得多。
第六步:把结果写成一张表,定期重跑
输出建议做成每站一行:字段行数、矛盾类别数、各类明细。存档之后每月重跑一次,只看数字有变化的那几行。响应头是很少变的东西,一旦变了要么是自己改了配置,要么是平台改了模板,两种都值得看一眼。
存档与巡检可以一条龙,备份、sitemap、缓存、证书都自动跑脚本可以直接改。
定期重跑交给定时任务,把巡检、推送和清缓存都自动化是很划算的投入。
量这批数据的时候,哪几个判定差点搞错?
照惯例交代口径,这一节是给想复现的人省时间的。
口径不清的指标最容易误导,从指标体系到异常诊断的完整做法可以拿来搭框架。
判据松紧决定结论性质,量出七成页面有差异、补上对照组后只剩两个点是同一类返工。
这个修正带来的连锁反应不小。69这个数字如果直接写进结论,会让读者以为将近一半的站有严重问题;而实际情况是其中大部分只是写法冗余,行为完全正确。判据的松紧直接决定了结论的性质,不只是决定数字大小。
第一处:把合并当成了矛盾
第一版脚本把所有同名重复字段都算成矛盾,数出来69处。后来意识到多数是Vary的两行,而按规范那是合法的合并写法,行为完全正确。
换个口径看数据结论可能完全不同,三类站点的聚合自然流量实测就是一次口径切换。
同一个名字各家算法不同到处都是,关键词难度为什么各家工具差那么大是最典型的一例。
修正之后的口径是:只统计取值不同的同名字段,并且在结论里明确标注这一类的下场是合并而不是冲突。数字从69降到57,更重要的是性质变了——它不是错误,是不整洁。
这一条在上一批也吃过亏,当时是把抓取失败的响应体也当成了有效样本,分母虚高了四份。两次的教训是同一个:抓取阶段和分析阶段必须分开,中间用一份带状态码的记录连接,让分析脚本自己决定哪些能用。
第二处:分母必须是最终的2xx响应
抓回来的文件里包含跳转链上的每一段响应。如果不加区分地统计,会把跳转响应的字段和最终页面的字段混在一起。跳转响应通常只有几个字段,会把中位数拉低;而且它的缓存策略跟最终页面完全不同。
样本干不干净得先验一遍,132个独立站首页有28个是空壳就是清洗之后的数字。
抽样口径直接决定结论真假,孤岛页面检测的抽样口径怎么定是同一类方法论问题。
所以脚本里第一步就是切出最后一段以HTTP开头的响应块,只有状态码是2开头的才进分母。142这个数字是这么来的,比抓取的站点总数少。
第三处:判断策略指令时要区分两种模式
内容安全策略有正式和只报告两个字段,它们的名字很像,取值语法也一样,但规则不同——有两条指令在只报告模式下被忽略。脚本一开始把两个字段合在一起解析,导致那4个站的问题没被识别出来。
报告里的状态词各有含义,五类问题URL的占比与排查顺序能帮你分清是谁的问题。
名字像的两个东西行为未必像,五类页面的meta robots与canonical配置差异可以直接照搬。
分开解析之后才发现只报告模式里的这类空转。这类细节的教训是:名字像的两个字段,行为未必像,写脚本时宁可分开处理。
第四处:一个PHP的低级坑
输出统计的时候,双引号字符串里变量名后面紧跟中文标点,那个标点会被当成变量名的一部分,PHP允许高位字节出现在标识符里。结果是最关键的那个数字变成空白,还附赠一条未定义变量警告。
有报错反而是好事,这个致命错误的五步排查修复至少有明确的起点。
脚本层面的小疏忽代价可能很大,上传目录还能跑PHP的五种加固写法是另一类低级但致命的问题。
这批犯了两次才反应过来,修法是所有插值一律用花括号包裹。写多语言输出的脚本时值得注意,它不报错,只是让数字消失。
为什么把这些写出来
因为69和57这两个数字放进文章里都很有说服力,读者没有办法判断哪个是对的。把口径、剔除规则、修正过程摊开,是唯一能让人放心引用的做法。
指标定义清楚才谈得上对比,三个平台三百个站的收录速度实测是一份口径统一的样本。
样本量大不代表结论可用,三千条数据揭开的认知差说明该信什么不该信什么。
常见问题解答
同一个响应头字段发两次,浏览器听哪一次?
两次都听。HTTP的字段语法规定同名字段多次出现等价于把取值用逗号连起来,所以两行Vary等于一行写了两个值。例外是Set-Cookie,它的取值本身含逗号,规范专门规定每行独立。真正会出事的是不允许列表值的字段发两次且取值不同,比如Content-Length,那属于协议错误。
Cache-Control和Expires同时写,以哪个为准?
以Cache-Control的max-age为准,Expires只在没有max-age时才被读。样本里8个站的两者说的不是一个时间,其中一个站的Expires写着1984年、另一个写着1970年,都是老年代表示立即过期的写法。建议只写Cache-Control,多一个字段就多一份需要长期同步的副本。
X-Frame-Options和frame-ancestors都写了会怎样?
支持后者的浏览器必须完全忽略前者,不是取交集也不是取更严格的。样本里10个站的两者不一致,包括一个X-Frame-Options写着谁都不许嵌、而策略里允许自家所有子域嵌的组合,那行deny等于没写。正确做法是让两者意图一致,老字段只写deny或sameorigin。
为什么我的X-Frame-Options写了一串网址却不生效?
因为这个字段只认两个取值,写别的属于非法取值,处理方式是整行丢弃。样本里5个站把内容安全策略的语法搬了过来,写成sameorigin后面跟几个具体地址,结果那行一个字都没生效。需要允许特定来源嵌套只能用frame-ancestors。
策略里同时写strict-dynamic和白名单域名有意义吗?
只有兼容意义。支持strict-dynamic的浏览器会忽略同一条指令里所有基于地址的来源,只信任带nonce或哈希的脚本;不支持的浏览器才退回到白名单。样本里3个站这么写,最长的一个列了82个域名。这么做可以,但必须在文档或部署脚本里注明它是兼容退路,否则会有人继续往那份死清单里加东西。
只报告模式的策略要注意什么?
两点。一是frame-ancestors和sandbox在这个模式下被规范忽略,写了也观察不到;二是必须先配好报告端点再写策略,否则既不拦截也收不到报告。样本里4个站踩了第一点,3个站踩了第二点,其中三个站两点都占。
清理这些矛盾能提升性能吗?
几乎不能。响应头字节数中位是3204,矛盾字段合计一百多字节,占比可以忽略。值得清理的理由是另外三个:减少审计噪声、避免你以为生效的那条其实没生效、以及把注意力腾给真正缺失的字段——样本里权限策略的覆盖率只有4.2%。
权威参考资料
本文标题:《HTTP响应头自相矛盾:142个站里108个在犯》
本文链接:https://zhangwenbao.com/http-response-header-self-contradiction-audit.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0