支付页CSP实测53个站:45个配了,只有5个管脚本
本文目录
- 支付页上那行安全策略,到底是谁写的?
- 这次量了哪些站,怎么量的
- 为什么87个站只剩56个
- 那17个403说明了什么
- 为什么用购物车页而不是结算页
- 整齐本身就是证据
- 为什么83.9%的覆盖率换不来83.9%的保护?
- 一条策略能管的事,取决于它写了哪些指令
- 另外那9个连策略都没有的站
- 那5个自己写的站,写成了什么样
- 为什么中间是真空
- 覆盖率这个指标该怎么改
- 那三条出厂指令各自管住了什么?
- block-all-mixed-content
- upgrade-insecure-requests
- frame-ancestors
- 还有一条谁都没写的指令
- 被防的那件事具体是怎么发生的
- 同一个平台的两代默认值,差别到底有多大?
- 这9个站现在处于什么状态
- 为什么点击劫持在电商站被长期低估
- 怎么判断自己拿到的是哪一代
- 为什么会有两代
- 你的结账页上跑着多少家公司的代码?
- 中位数7个,最多18个
- 长尾比头部更难管
- 脚本数量本身也是个信号
- 长尾里的三类值得单独看
- 一个域名背后往往不止一个第三方
- 第三方为0的那些站是怎么做到的
- 页面上真正的大头,为什么是内联脚本?
- 2711对1422
- 内联脚本是两套机制的共同盲区
- nonce这条路要付出什么
- 把内联块当成资产来管
- 内联脚本从哪儿来
- 违规报告为什么一条都收不到?
- 报告端点是干什么用的
- 先用仅报告模式跑一段
- 报告收到的东西长什么样
- 回执默认关闭,是个反复出现的形态
- 那79个完整性校验是谁加上去的?
- 39个站有,其中38个恰好是2个
- SRI和CSP来自同一个模板
- 那两个哈希指向的是什么
- 为什么这条线这么难铺
- PCI DSS 6.4.3和11.6.1要你交出什么?
- 6.4.3要的是一份清单
- 11.6.1要的是一个会响的机制
- 两条是一对,不是两件事
- 自评问卷选哪一份,答案在这一页上
- 范围问题比技术问题更容易踩坑
- 评估时实际会被问到什么
- 怎么把范围真正缩小
- 出厂默认为什么总是偏向放行?
- 平台面对的约束和你不一样
- 反过来的例子:也有偏向收紧的默认值
- 用另外两条响应头验证这个判断
- 代填的东西出了事算谁的
- 一个真实的时间线
- 30秒怎么判断自己是不是那42个之一?
- 第一件:这一行有多长
- 第二件:里面有没有script-src这十个字符
- 第三件:有没有report-uri或者report-to
- 把这三条做成一次性的自查表
- 脚本清单基线怎么落地才不变成一次性文档?
- 清单里放什么
- 第一版怎么在两小时内做出来
- 比对怎么自动化
- 内联脚本怎么进清单
- 同意状态会让同一页有好几副面孔
- 告警该发给谁
- 三个常见的坑
- 做完这一轮之后,下一步该看哪儿
- 这条判据还能搬到哪儿
- 常见问题解答
- 购物车页和真正的支付页是一回事吗?
- 那9个拿到旧模板的站,能自己升级到新模板吗?
- 用了托管支付或者纯iframe嵌入,是不是就不用管这两条要求了?
- 加了integrity之后第三方脚本更新导致页面出错,怎么办?
- 只把策略加严,不做清单,行不行?
- 怎么判断某个第三方脚本是不是真的需要留在这一页上?
- 体检工具报的“已配置内容安全策略”能不能直接当结论?
- 换一个建站平台能解决这个问题吗?
- 安全策略写严了会不会影响转化率?
- 这批数据能代表整个行业吗?
- 权威参考资料
摘要:把87个DTC与电商站的购物车页整个拉了一遍,56个拿到了可分析的真页面。其中47个带了内容安全策略响应头,覆盖率83.9%看着不难看;可这47条策略里有42条的长度落在49到175字节之间,翻出来是同一句话,一个字没改过。真正写了script-src、对页面上能跑谁的脚本有约束的,只有5个站。配了违规报告端点的,0个。
先把动作说清楚。给每个站的/cart发一次普通的浏览器请求,跟完跳转,把最后那一次的响应头和HTML原样存下来,然后只数三样东西:响应头里有没有安全策略、策略里写了什么、页面上挂了多少别人家的脚本。没有登录,没有加购,就是一个陌生人打开购物车页会看到的样子。
结果整齐得有点吓人。而整齐本身就是结论:当几十个互不相干的品牌在同一个位置写出一模一样的一句话时,那句话就不是他们写的。
支付页上那行安全策略,到底是谁写的?
先看这条策略长什么样。下面这一行,是30个站原样返回的内容:
支付这条线上还有一批同样只做一次就没人碰的配置,从注册到运营的合规底座八步把它们串在了一起。
content-security-policy: block-all-mixed-content; frame-ancestors 'none'; upgrade-insecure-requests;75个字节,三条指令,末尾一个分号。另外9个站把中间那条换成了frame-ancestors *,长度70字节,其余完全一致。还有零星几个是别的短句:一个49字节,一个71字节,一个175字节。
这条指令配错的后果非常隐蔽,服务器全程返回二百、用户眼前却一片空白复盘了一次被它拦住渲染的事故。
跟协议相关的另一件正在逼近的事是证书有效期,证书有效期正在砍向四十七天说明了续期流程该怎么改。
明文协议残留是老站的通病,网站从明文迁到加密协议怎么才能稳住排名里有一份完整的迁移与回归清单。
把47条策略按长度排一排,分界线清楚得不像统计结果:
| 策略长度 | 站数 | 里面有没有script-src | 内容形态 |
|---|---|---|---|
| 49到175字节 | 42 | 没有 | 两三条出厂指令 |
| 4469到12946字节 | 5 | 有 | 逐个域名列出来的白名单 |
| 没有这个响应头 | 9 | — | — |
中间是空的。没有任何一个站的策略长度落在176到4468之间。这说明这件事没有中间状态:要么你从来没碰过它,拿到的是平台装好就有的那两三行;要么你真的坐下来把每一个域名列了一遍,写出来至少四千多字节。没有人写到一半。
这次量了哪些站,怎么量的
样本是87个以DTC品牌为主的独立站与电商站,涵盖服饰、床品、家居、户外、宠物食品、个护、3C配件、厨具几个品类,绝大部分是北美市场的品牌自营站,另外放了几个平台型站点做对照。每个站发三个请求:购物车页、带广告参数的首页、robots.txt。这篇只用第一个,另外两个留给这一组的另外两篇。
同一批站要横向比,抽样口径必须先定死,这套做法在电商站按页面模板抽样做审计那篇里拆得更细。
请求用一个普通Chrome的User-Agent,跟随重定向,允许压缩,超时30秒。响应头取的是跳转链末端那一次——这一点很关键,中间那些301的头没有意义,浏览器最终执行的是最后那一份。
为什么87个站只剩56个
31个站没拿到可分析的页面,分布如下:
批量抓站被挡是常态,桌面爬虫那一套怎么配才不被拒,可以看全站爬取审计的十二类问题排查清单。
| 情况 | 站数 | 说明 |
|---|---|---|
| 403被防护挡下 | 17 | 人机验证或风控直接拒绝 |
| 404或购物车不在这个路径 | 7 | 有的做成了/basket、/bag,有的整站没有购物车页 |
| 429限流 | 2 | 请求频率被判定过高 |
| 连接失败 | 2 | 握手阶段就断了 |
| 200但只是个壳 | 1 | 页面正文全靠脚本后填 |
| 跳到完全无关的地方 | 2 | 一个跳到国内某电商的店铺页,一个域名已挂牌出售 |
最后那两个值得单独说一句。一个北美厨具品牌的购物车地址,最后停在了国内某电商平台的店铺页;另一个曾经很有名的剃须刀DTC品牌,域名现在挂在域名交易平台上等人报价。做竞品调研时按老名单批量拉数据,这两条会安静地混进你的样本,而且不会报任何错。它们的HTTP状态码是200和403,抓取脚本看不出异常,只有人眼看一眼落地页才发现不对。
那17个403说明了什么
17个站在购物车页上直接拒绝了一个普通浏览器UA的匿名请求。这批站分布在防护服务、边缘风控和自建限流三类上,共同点是它们把购物车页当成了需要保护的资源。这本身是个信号:购物车页在这些团队眼里已经不是一个普通的内容页,而是一个交易入口。可惜的是,把外部访问挡在门外和把页面内跑的脚本管起来,是两件完全不同的事,前者做了不代表后者做了。
防护规则挡住的往往不只是坏人,防火墙到底拦不拦得住爬虫那篇里量过误伤的比例和它的代价。
为什么用购物车页而不是结算页
严格说,标准里管的是“支付页”,也就是用户输入卡号那一屏。但那一屏几乎不可能用一次匿名请求拿到:它需要真实加购、真实会话,很多站还要求填地址走完一步。要横向比较56个站,只能选一个所有站都存在、都能匿名访问、且在合规范围判定上跟支付页强相关的页面。购物车页是最接近的那个。
结账那一段本身就是流失最重的地方,独立站结账页放弃率超七成的九个真实成因列了具体是哪几处在漏人。
更重要的是,这一页在实际风险里的位置比想象中靠前。用户在购物车页上已经暴露了完整的购买意图和商品明细,很多站还会在这一页展示已保存的地址与优惠码。而从攻击角度看,购物车页是通往结算页的最后一跳,能改这一页的人就能改那个跳转的去向。
整齐本身就是证据
如果这42个站是各自独立做的安全决策,长度分布不可能长这个样子。安全策略这种东西一旦有人真动手写,写出来的长度取决于他家挂了多少个第三方,必然离散。42个站挤在49到175这个区间里,只有一种解释:这一格是建站平台在他们开店那天就填好的,此后没有任何人回来看过。
一堆来源不明却高度一致的东西是个危险信号,十条最佳实践清单里八条数字对不上说的是同一种病。
为什么83.9%的覆盖率换不来83.9%的保护?
“有没有配置内容安全策略”是个很好数的指标,也是各种安全体检工具最爱报的指标。按这个口径,这批站的成绩是47除以56,83.9%,相当漂亮。
同一个指标换个口径就变个样,排名监测为什么老对不上用六个原因说明了口径差异有多大。
问题在于这把尺子量的是“这一行存不存在”,不是“这一行管住了什么”。
一条策略能管的事,取决于它写了哪些指令
内容安全策略不是一个开关,是一组分门别类的白名单。script-src管脚本从哪儿加载,connect-src管页面能往哪儿发请求,form-action管表单能提交到哪儿,frame-ancestors管谁能把你嵌进去。你没写的那一类,就是不设限。
白名单写了也未必稳,白名单上明明写着那个域名浏览器还是把它拦了记录了一次实际的漂移事故与排查过程。
把47条策略里出现过的指令按频次排开,能直接看到平台填了什么、人填了什么:
| 指令 | 出现次数 | 谁填的 |
|---|---|---|
| frame-ancestors | 49 | 出厂指令之一 |
| upgrade-insecure-requests | 41 | 出厂指令之一 |
| block-all-mixed-content | 40 | 出厂指令之一 |
| frame-src | 12 | 自己写的那几家 |
| script-src、connect-src、img-src、style-src、default-src、base-uri | 各11 | 自己写的那几家 |
| worker-src | 10 | 自己写的那几家 |
| form-action、child-src、script-src-attr | 各6 | 自己写的那几家 |
| object-src | 2 | 自己写的那几家 |
看这张表的时候盯住那道断崖:前三条都在40以上,第四条掉到12。前三条是平台给的,第四条往后全是人写的。指令的频次不反映它有多重要,只反映它在不在出厂配置里。
另外那9个连策略都没有的站
47个有策略的站之外,还有9个连这个响应头都不返回。这9个站里没有一个是随便做的小站,都是有一定规模的品牌。
翻它们的其他响应头能看出原因:这9个里有4个连加密强制和内容类型嗅探保护也一起缺着,说明它们跑在一套完全没有配安全头的基础设施上;另外5个有别的安全头,唯独没有内容安全策略,更像是主题或者网关升级时被漏掉的。
值得注意的是,这9个站里有6个的页面上第三方脚本域名少于3个,明显低于整体中位数。这不是巧合:它们大多是自建前端、服务端渲染、把第三方逻辑收到后端的那一类站。没有策略和不需要策略,在这里居然有一点重合——第三方越少,缺策略的实际暴露面就越小。但这只是暴露面小,不是要求不适用,评估时同样要拿出脚本清单。
那5个自己写的站,写成了什么样
把这5个站的策略单独拎出来看,能学到的东西比看那42个多得多。
同一类加固动作在别处也会误伤,照着清单加的一行响应头把地图和支付按钮一起变成摆设是个值得先看的反面案例。
| 站的类型 | 策略长度 | 'unsafe-inline' | 'unsafe-eval' | 报告端点 | 页面第三方脚本域名数 |
|---|---|---|---|---|---|
| 自有品牌电商 | 4469 | 有 | 有 | 无 | 4 |
| 家具品牌 | 6701 | 有 | 无 | 无 | 0 |
| 饰品品牌 | 7080 | 有 | 有 | 无 | 0 |
| 摄影配件品牌 | 8644 | 有 | 有 | 无 | 1 |
| 个护品牌 | 12946 | 有 | 有 | 无 | 0 |
五个全都放开了'unsafe-inline',四个放开了'unsafe-eval'。这两个关键字的意思很直白:允许执行写在HTML里的内联脚本、允许把字符串当代码执行。一旦放开'unsafe-inline',那份辛苦列了几千字节的域名白名单,对“页面上被注入了一段内联脚本”这种最常见的攻击就没有约束力了。
这不是说这五家做得不好,恰恰相反,他们是这批站里唯一真做了事的。'unsafe-inline'几乎是历史包袱的必然产物:老主题里到处是内联的事件处理和统计代码片段,一刀切掉页面就废了。真正的路是用nonce或者哈希把内联脚本一条条放行。这五家里有两家已经写了nonce,只是同时把'unsafe-inline'也留着——按script-src指令的规范说明,浏览器只要看到nonce就会忽略'unsafe-inline',所以这两家实际上是收紧了的,另外三家不是。
这条规则值得单独记一下,因为它是这套机制里少见的“写了两条互相矛盾的规则、结果按严的执行”的设计。它的用意是让老站能平滑迁移:先把'unsafe-inline'留着保证不出事,再逐步给内联脚本补nonce,补完之后'unsafe-inline'自动失效,不需要再回头改一次策略。
为什么中间是真空
那道从175字节直接跳到4469字节的空白,值得再解释一层,因为它决定了这件事的改造成本长什么样。
不是所有安全动作都要一次到位,先做性价比高的那几件,按性价比排序的速赢清单给了一个排序方法。
写这条策略没有中间档,是因为它的语义是“白名单”而不是“黑名单”。你一旦写下script-src,页面上所有没被列进去的脚本立刻全部失效。也就是说,你不能先列三个域名试试水,那样剩下的十几个当场就废了。要么不写,要么一次性把所有该放行的都列全——这是个全有或全无的动作,所以产出物的长度必然要么是零,要么是几千字节。
理解了这一点,改造路径就很清楚:不要指望“先加一点点”。真正的最小步骤是先开仅报告模式,让浏览器帮你把该列的东西列出来,然后一次性上线。把这件事的成本理解成一个台阶而不是一个斜坡,排期的时候就不会一直往后拖。
覆盖率这个指标该怎么改
如果一定要用一个数来汇报,别汇报“配了策略的比例”,汇报下面这个:
分母定义不同,同一份数据能给出完全相反的结论,审计工具号称的九成五准确率是自己划的及格线讲的就是这件事。
策略里出现script-src或者default-src的页面数,除以需要保护的页面总数。
按这个口径重算:5除以56,8.9%。同一批站,同一天的数据,从83.9%变成8.9%。两个数都没算错,差别只在于前一个问的是“有没有这一行”,后一个问的是“这一行拦不拦得住脚本”。
顺带说一句,为什么用default-src也算数。规范里script-src没写的时候,脚本会回退到default-src的白名单,所以只写了default-src的策略同样有约束力。这批样本里写了default-src的11次全部来自那5个站,没有出现“只写default-src不写script-src”的情况,但在别的技术栈上这种写法很常见,判定时别漏。
那三条出厂指令各自管住了什么?
既然42个站的全部保护就是这两三条,那就得把它们到底管什么说清楚。它们都不是废话,只是管的都不是脚本来源。
响应头之外,页面结构本身也有一批被忽略的默认值,八类语义化标签对搜索的真实影响逐条量过。
block-all-mixed-content
禁止在HTTPS页面里加载HTTP的资源。这条在十年前很有用,那会儿到处是写死了明文协议的图片和脚本。今天它基本是历史遗留:现代浏览器早就默认阻断混合内容里的脚本类资源,规范本身也已经把这条标记为废弃,功能被下面那条吸收了。它出现在40个站的策略里,纯粹是因为模板从来没更新过。
upgrade-insecure-requests
把页面里所有明文协议的请求自动改写成加密协议再发出去。这条今天仍然有用,尤其是老主题、老邮件模板、老第三方SDK里混着裸HTTP地址的时候。但它管的是协议,不管来源——把一个恶意域名上的脚本从明文升级成加密,它照样会加载并执行,而且升级完之后连浏览器的混合内容警告都没有了。
frame-ancestors
规定谁可以把你的页面嵌进iframe。设成'none'就是谁都不许,用来防点击劫持。这条是三条里最有实际价值的一条,但它防的是“你的页面被别人套进去”,不是“别人的代码被套进你的页面”。方向正好相反。
把三条并排放,问题一目了然:
| 指令 | 管的是 | 对“页面上多出一个脚本”有没有约束 |
|---|---|---|
| block-all-mixed-content | 资源用的协议 | 没有 |
| upgrade-insecure-requests | 资源用的协议 | 没有 |
| frame-ancestors | 谁能把你嵌进去 | 没有 |
三条加起来,对“有人往这一页塞了一段读表单的脚本”这件事,约束是零。而这恰好是支付页面临的头号风险类型:攻击者不需要拿到你的服务器,只需要让浏览器多执行一段代码,就能在用户按下提交之前把输入框里的内容复制一份发走。页面照常工作,订单照常成交,用户和你都看不出任何异常。
还有一条谁都没写的指令
47条策略里,写了form-action的只有6条,全部来自那5个自己写策略的站。这条指令规定表单能提交到哪些地址。
表单这一块值得单独花时间,下拉框在独立站表单里悄悄吃掉询盘和订单从转化角度拆了控件选择的账。
它之所以值得单独提,是因为它防的正是支付场景里最直接的一种攻击:不改任何脚本,只把表单的提交地址改掉。用户填完卡号点提交,数据先去了别处再回来。页面上看不出任何异常,订单也照常创建,因为攻击者会把数据转发回原来的地址。这类手法在真实案例里出现过很多次,而防它只需要一行form-action。
这一条同样不在任何出厂模板里。它需要你知道自己的表单该往哪儿提交——这个知识只有你有,平台没有。凡是需要业务知识才能填的格子,出厂默认一定是空的,因为平台填不了。这条规律可以直接用来预测:看一眼哪些指令依赖你的业务信息,那些就是你必须自己动手的。
被防的那件事具体是怎么发生的
把攻击过程摊开看,就明白为什么这三条指令帮不上忙。
第三方代码带毒进来的路径不止一条,破解版插件的后门与被移出索引的代价记录了另一条更常见的入口。
典型的路径有三条。第一条是从第三方进来:你装了一个应用,应用的脚本托管在服务商的域名上,服务商的构建环境或者存储桶被入侵,脚本内容被加了几十行。你的页面上什么都没改,加载的还是同一个地址。第二条是从主题进来:主题模板里被塞了一段内联代码,通常伪装成统计片段。第三条是从标签容器进来:有人拿到了容器的编辑权限,加一个自定义HTML标签就完事,这条路甚至不需要碰你的代码仓库。
代码跑起来之后做的事都差不多:监听表单的输入事件或者提交事件,把输入框里的值拼成一个字符串,通过图片请求或者信标接口发到外部地址。整个过程不改变页面外观,不影响订单流程,用户填完照样能付款成功。这也是为什么它常常几个月才被发现,而发现的渠道往往是发卡行的欺诈模型报警,不是商户自己的监控。
对着这三条路回看那三条出厂指令:第一条路走的是白名单里已有的域名,没有script-src也就无所谓拦不拦;第二条和第三条走的是内联,白名单从设计上就管不着。三条指令一条都不在这几条路径上。
同一个平台的两代默认值,差别到底有多大?
前面提到42个出厂式策略里有两种写法,一种是frame-ancestors 'none',一种是frame-ancestors *。这两个值的语义正好相反:前者是谁都不许嵌,后者是谁都可以嵌。
两套配置同时生效是改版后的高频事故,插件和主题打架冒出两个规范标签怎么归一给了归一顺序。
把这两组站跟另一个防点击劫持的老响应头X-Frame-Options交叉一下,结果非常干净:
| 分组 | 站数 | 其中带X-Frame-Options的 |
|---|---|---|
| frame-ancestors 'none' | 30 | 30 |
| frame-ancestors * | 9 | 0 |
| 其他写法 | 8 | 2 |
| 没有策略 | 9 | 4 |
30个站全有,9个站全无。这不是巧合,也不是这9家的安全水平更差,而是他们拿到的是同一个平台的另一代默认模板。两代模板一起换了两样东西:策略里的那个值,和那个额外的响应头。
这9个站现在处于什么状态
两道防点击劫持的门同时开着。有人可以把这9个品牌的购物车页嵌进自己的页面,盖一层透明的覆盖物,诱导用户点在他想让你点的位置上。这类攻击在电商场景里最常见的用法不是偷钱,是刷动作:把“加入购物车”“订阅邮件”“授权登录”这类按钮盖在一个看起来无害的按钮下面。
模板一换,看不见的配置会成批消失,换个主题改个版流量为什么就掉了列了一份改版前必须留底的清单。
这9个站要修其实很简单,加一个响应头或者把那个星号换掉就行,成本是分钟级的。真正的问题是没有人会发现——因为从商家后台看,这一格根本不存在,它不在任何一个设置页面上。
为什么点击劫持在电商站被长期低估
这类风险之所以没人重视,是因为它的收益路径不直接。攻击者盖一层透明的东西在你的按钮上,用户点下去的既不是钱也不是密码,看起来没什么可偷的。
被劫持的那些按钮往往也是转化路径上的关键节点,购买路径上那些看不见的摩擦力逐个点过它们的位置。
但换个角度算这笔账就不一样了。能被劫持的动作里,价值最高的三个是授权登录、订阅确认和一键加购。第一个能拿到一次社交账号的授权,第二个能把一个真实用户塞进别人的名单,第三个配合联盟链接能把佣金记到别人头上。这三件事都不需要用户输入任何东西,只需要他点一下。
更现实的一种用法是刷数据。把一个高流量页面嵌进去,用透明层诱导点击,短时间内可以把某个商品的加购数、某个活动的参与数刷到很高。这类数据一旦进了你的报表,后面所有基于它的选品和投放决策都是错的,而你根本不知道该怀疑哪一天的数据。
所以判断要不要修这件事,不该按“会不会被偷钱”来算,该按“我的哪些按钮点一下就产生业务后果”来列。列完就会发现,一个成熟的电商站上这样的按钮通常有十几个。
怎么判断自己拿到的是哪一代
一条命令就够:
用命令行查响应头有个经典陷阱,curl -I查了一圈说没配缓存头换成GET之后五个头全在值得先读一遍再动手。
curl -sI https://你的域名/cart | grep -iE 'frame-ancestors|x-frame-options'看到frame-ancestors *并且没有第二行输出,就是那9个里的一个。这个判断不需要任何安全知识,看星号就行。
为什么会有两代
平台的默认模板会随版本演进,而已有店铺往往不会被强制升级到新模板——升级默认值有把现有页面搞坏的风险,平台通常只对新开的店应用新默认。于是同一个平台上,开店时间不同的商家跑着不同的出厂配置,而这件事对商家完全不可见。
平台选择带来的差异常被高估也常被低估,搜索引擎偏爱某个建站平台吗把这件事拆成了可验证的几条。
这条规律比这个具体案例更值钱:凡是“注册时发一份默认配置、之后各自演化”的系统,你的默认值版本取决于你什么时候开的户,跟你的业务规模、付费档位、安全需求全都无关。做尽调、做并购整合、接手别人维护的站点时,这一条是最容易被跳过的检查项。
你的结账页上跑着多少家公司的代码?
把56个页面里所有带src的script标签的域名抽出来,去掉本站自己的,剩下的就是别人家的代码。
下一页的风险同样值得看一眼,用户付完钱看到的那一页其实最容易出事拆了订单确认页该有哪几块。
中位数7个,最多18个
56个站合计出现124个不同的第三方域名,第三方脚本域名一共出现368次,每站中位数7个。最多的一个旅行箱品牌,购物车页上挂了18个不同域名的脚本。有8个站是0个,这些站要么把第三方逻辑放进了服务端,要么把脚本打包进了自己的域名。
应用装多了不只是安全问题,店铺装太多应用拖慢速度还烧钱给了一套精简与冲突排查的顺序。
出现频次最高的那些域名,基本可以当成这个赛道的技术栈快照:
| 第三方域名 | 出现在几个站 | 做什么的 |
|---|---|---|
| cdn.shopify.com | 41 | 建站平台自身资源 |
| shop.app | 39 | 平台的快捷支付与追踪 |
| static.klaviyo.com | 30 | 邮件与短信营销 |
| cdn-widgetsrepository.yotpo.com | 15 | 评价与忠诚度组件 |
| cdn.attn.tv | 12 | 短信获客弹层 |
| config.gorgias.chat | 11 | 客服工单与聊天 |
| cdn.cookielaw.org | 10 | Cookie同意管理 |
| cdn-static.okendo.io | 8 | 评价采集 |
| cdn.rebuyengine.com | 7 | 推荐与加购弹层 |
| assets.visually.io | 7 | 页面实验与个性化 |
| content.9gtb.com | 7 | 客服组件的配套链路 |
| static.shopmy.us | 7 | 达人带货追踪 |
长尾比头部更难管
124个域名里,只出现在1个站上的有77个,占62.1%;出现在5个站以上的只有19个。这意味着行业级的“常见第三方清单”这种东西对你没什么用——你要管的那六成,别人都没装。
长尾工具的年度盘点有现成方法,十二个站样本里的工具栈冗余与八步瘦身可以直接照着做一遍。
这77个长尾域名里能看出很多东西:有做尺码推荐的、有做礼品卡的、有做订阅管理的、有做无障碍浮层的、有做防欺诈的、有做本地化定价的、有做视频购物的、有做限购规则的。还有几个是对象存储的原始域名,比如某个云厂商的存储桶地址直接出现在script标签里。存储桶域名出现在支付相关页面上是个值得警觉的信号,因为它的内容可以被任何有写权限的人替换,而域名本身不会变。
脚本数量本身也是个信号
把每个站的第三方域名数和它的页面体积放在一起看,相关性很强但不是必然。体积最大的那个站有14个第三方域名,页面接近6兆;体积最小的那个只有33千字节。中位数在462千字节左右。
脚本堆积最先撑爆的是页面体积,页面体积实测四十六个电商站量过体积超标之后哪些内容会被截断。
更值得看的是分布形状:第三方域名数从0到18连续分布,没有明显的分组。这跟前面策略长度那个双峰形成鲜明对照——策略是有人写没人写的二元选择,装应用是一个个累加的连续过程。没有哪个团队坐下来决定“我们要装14个第三方”,都是一年里每个月装一个装出来的。
这个形状对治理的启发是:清单这件事必须做成流程里的一道闸,而不是一次性的清理项目。一次性清理能把14降到9,但如果不改变“装东西不用登记”这件事,一年后它还会回到14。这也是标准把6.4.3和11.6.1写成一对的原因——前者是清理,后者是闸。
长尾里的三类值得单独看
把那77个只出现一次的域名扫一遍,有三类特别值得注意。
把一堆地址批量归成根域名是这类盘点的第一步,域名提取器怎么用讲了去重口径怎么定。
第一类是对象存储的原始地址。样本里有好几个站的script标签直接指向云厂商的存储桶域名。这种地址的特点是内容随时可以被替换,而域名一个字都不会变——白名单挡不住,只有内容哈希挡得住。更麻烦的是,存储桶的写权限往往握在某个外包团队或者某个已经离职的人手里。
第二类是通用CDN上的公共库。样本里出现了几个知名的公共CDN域名,上面挂的是通用的JavaScript库。把公共库放在公共CDN上,等于把执行权交给了一个你没有任何合同关系的第三方。业内出过不止一次公共库被投毒的事件,最著名的一次影响了十万级的站点。这类地址是最容易改也最该改的:下载下来放自己域名,一次性成本半小时。
第三类是明显的试验残留。有几个域名从名字就能看出是某次活动、某次测试或者某个已经不用的工具留下的。它们还在页面上,还在执行,只是没有人再看它们的数据了。这类脚本的风险不在于它现在做什么,在于它的服务商可能已经不维护那个域名了——域名一旦过期被别人注册,页面上就多了一段任何人都能改的代码。
一个域名背后往往不止一个第三方
上面这些数字还低估了实际情况。标签容器这一类东西是脚本的脚本:页面上只出现一个容器域名,容器里可以配十几个代码片段,运行时再去拉别的域名。同样,客服组件加载完之后往往会再拉一次自家的配置和一次第三方知识库。你在HTML里数到的域名数,是这条链的第一层,不是全长。
域名归属的判定比看上去复杂,后台一个域前端一个域浏览器认不认它们是同一个站解释了边界是怎么划的。
这批样本里有一个站同时挂着三个属于同一家客服服务商的不同域名,它们是三条不同的链路。如果你的清单按“服务商”维护,这里记一条;按“域名”维护,记三条。做脚本清单时这两种口径必须选一个并写清楚,否则下次比对时会凭空多出两条“新增”,然后没人敢下结论。
第三方为0的那些站是怎么做到的
8个第三方脚本为0的站,路数不太一样。有几个把购物车做成了服务端渲染的页面,脚本全部同域打包;有几个用了自建前端框架,第三方SDK在构建期就打进了自己的包;还有几个干脆把购物车做成一个几乎没有交互的中转页,用户点结算就整页跳到托管的支付域名去了。
把支付托管出去之前,先看清各地买家习惯用什么付款,独立站支付方式怎么配才有人肯付款有一份分地区的对照。
最后这一种最值得学。把真正碰卡号的那一屏整个交给支付服务商托管的域名,你自己的页面上就不再有需要按支付口径管起来的脚本。这不是偷懒,是把责任边界画在了正确的地方,代价是那一屏的样式和体验你改不动多少。这笔账在转化率和合规成本之间怎么算,取决于你的客单价和评估等级。
页面上真正的大头,为什么是内联脚本?
前面数的都是带src的外部脚本,但页面上的script标签还有另一半,是直接把代码写在HTML里的内联脚本。
页面里跳过渲染的手段也在往这个方向走,渲染跳过与样式隔离的机制解释了浏览器省下的是哪部分开销。
2711对1422
56个页面上一共4133个script标签,其中外部的1422个,内联的2711个,内联占65.6%。内联脚本的体积中位数是176362字节,最大的一个站达到1696880字节——单页面里的内联脚本代码就有1.6兆多。
内联块堆到这个量级,首屏一定会被拖住,关键渲染路径怎么优化拆了阻塞渲染的资源该怎么排队。
| 站的类型 | 外部脚本 | 内联脚本 | 内联字节 |
|---|---|---|---|
| 口腔护理品牌 | 21 | 111 | 464201 |
| 女鞋品牌 | 51 | 110 | 615043 |
| 气泡饮料品牌 | 40 | 88 | 236380 |
| 服饰品牌 | 37 | 86 | 158708 |
| 卫浴品牌 | 41 | 85 | 280855 |
| 内衣品牌 | 71 | 74 | 553218 |
内联脚本是两套机制的共同盲区
这个比例之所以重要,是因为内联脚本正好卡在两条防线的缝里。
完整性校验自己也会出事,脚本下载完了一行都没执行而拦住它的是三个月前那串哈希复盘了一次这样的翻车。
第一条防线是内容安全策略的白名单。白名单管的是“从哪个域名加载”,内联脚本没有域名,它由'unsafe-inline'、nonce或者哈希来管。前面看到那5个自己写策略的站全都放开了'unsafe-inline',也就是说这条防线对内联脚本实际上是敞开的。
第二条防线是子资源完整性校验。它的做法是给script标签加一个内容哈希,浏览器下载完对不上就不执行。而内联脚本根本没有“下载”这一步,integrity属性对内联脚本无效,规范里就没有这个用法。
两条防线都不管,而页面上65.6%的script标签属于这一类。这就是为什么行业里针对支付页的那类攻击,最经典的手法不是替换某个外部脚本文件,而是往页面HTML里插一小段内联代码——插进去之后没有任何客户端机制会拦它。
nonce这条路要付出什么
要真的管住内联脚本,只有两种办法:给每一个内联块算哈希写进策略,或者给每一个内联块发一个一次性的随机串。前者适合内联块固定不变的站,后者适合内容动态生成的站。
整页缓存和一次性随机串天生冲突,全页缓存怎么配才不出事里的缓存键设计可以顺带解决这个矛盾。
随机串这条路的代价在服务端。每次请求生成一个新的随机值,写进响应头的策略里,同时写进页面上每一个内联script标签的属性里。这就要求页面必须是动态渲染的——如果整页被CDN缓存,随机值也会被缓存,那它就不再是一次性的,安全价值归零。
这个约束会直接影响架构选择。一个整页静态化、靠边缘缓存扛流量的站,要么放弃随机串走哈希路线,要么把策略头改成由边缘节点动态注入。哈希路线的麻烦是每次改一个字都要重算,得把这一步接进构建流程。两条路都不是加一行配置能解决的,这也是为什么这批样本里五个自己写策略的站全都留着放行内联的那个关键字。
务实的中间态是分阶段:先把页面上的内联块按来源分类,把平台和主题生成的那批算哈希固定下来,把标签容器注入的那批限制到一个专用的容器策略里,最后再处理零散的自定义片段。这个顺序能让每一步都有可见的收益,不至于做到一半停下来什么都没落地。
把内联块当成资产来管
换个视角能让这件事好办很多:不要把内联脚本当成“页面里的一段代码”,把它当成一件有归属、有版本、有生命周期的资产。
把零散的东西变成可管理的资产,第一步永远是先定口径,数据分析怎么入门里那三类工具的分工可以直接借用。
这样一来问题就变得可回答了。这一块是谁放的?什么时候放的?它依赖页面上的哪些变量?如果删掉会坏什么?这四个问题里,第三个最能筛出真正的风险块——凡是要读页面上其他数据才能工作的内联脚本,都有能力读到表单里的输入。
实际盘一遍会发现,大部分内联块只是在给某个变量赋值,或者在页面加载完之后触发一次异步请求,它们既不读表单也不改结构。真正需要盯的通常只有三五块,而这三五块往往就是当初为了“加个小功能”临时塞进去的。把范围从两千多个块缩到三五块,这件事才有可能被排进迭代。
内联脚本从哪儿来
拆开看,这两千多个内联块的来源大致三类。一类是建站平台和主题生成的初始化代码,比如把商品数据、购物车状态、货币设置写成JSON塞进页面;一类是各个应用装上之后注入的配置片段,通常是几行赋值加一次异步加载;还有一类是营销团队通过标签容器加进去的自定义代码,这一类最难追溯,因为它不在代码仓库里。
标签容器里的自定义代码多半跟追踪参数有关,追踪参数怎么打才规范能减少一部分临时脚本的产生。
第三类是清单工作真正的难点:它的变更不走发版流程,也不在任何一个技术人员的视野里。实务上见过最多的一种情况是,某个投放同学为了测一个新渠道的转化,在标签容器里加了一段代码,测完忘了删,一年后没有人知道那段代码在干什么,也没有人敢删。
违规报告为什么一条都收不到?
这是这次数据里最干脆的一个数字:47条策略里,配了report-uri或者report-to的,0条。另外有1个站配了仅报告模式的响应头,但同样没有配接收地址。
上报缺失会直接变成报表里的一块黑箱,直接流量突然暴增的六类成因是同一种缺口在另一处的表现。
报告端点是干什么用的
浏览器在拦下一个违反策略的资源时,可以顺手往你指定的地址发一个JSON,说明是哪一页、哪条指令、哪个地址被拦了。这是整套机制里唯一一条从用户浏览器流回你这边的通道。
把回执接进已有的告警体系比单独建一套划算,搭一套监控告警体系在掉量前抓住事故给了分层的做法。
没有它,你的策略就是个哑巴:拦对了你不知道,拦错了你也不知道,直到某个用户投诉“评价区加载不出来”。更要命的是,如果策略压根没写script-src,那连“拦下了什么”这件事都不存在,报告端点配了也收不到东西。这两件事是有先后的:先有约束,才有违规,才有报告。42个站卡在第一步。
先用仅报告模式跑一段
仅报告模式的响应头存在的意义就是给你一个不拦截的观察期:策略照常评估,违规照常上报,但不阻断任何资源。对一个从来没写过script-src的站来说,直接上拦截模式几乎必然出事,因为你根本不知道页面上到底有多少东西。
观察期这个概念在别处也一样管用,三十个实验方案从按钮到结账里的灰度节奏可以直接借过来。
实务上的节奏是:仅报告模式跑一到两周,把上报的域名去重,跟业务确认哪些是该留的,写进白名单,再切成拦截模式。这个过程里最花时间的不是技术,是找人认领那些谁也说不清的域名。
报告收到的东西长什么样
浏览器发过来的那个JSON很小,关键字段就几个:被拦的页面地址、触发的那条指令、被拦的资源地址、以及页面上的referrer。对排查来说够用了,但有两个已知的坑值得提前知道。
噪音淹没信号是所有上报类数据的通病,报表里的机器流量怎么揪出来再拦掉讲了几种可复用的过滤思路。
第一个坑是资源地址会被截断。出于隐私考虑,跨域被拦的地址通常只报到域名一级,路径和查询串被抹掉。这意味着你能知道“某个陌生域名被拦了”,但不知道它加载的是哪个文件。排查时得配合页面本身的抓取结果一起看。
第二个坑是量。一个中等流量的电商站开了报告之后,第一天收到几万条是常态,其中绝大多数来自浏览器扩展、运营商注入和用户装的各种插件——这些都不是你的问题,但它们会淹没真正的信号。接收端第一版就要按域名做聚合并且只保留每天每域名一条,否则这个功能会在第三天被人关掉。
回执默认关闭,是个反复出现的形态
这件事不止发生在浏览器这一层。往上看,几乎每一个“你写规则、别人执行”的机制都长这样:规则是你写的,执行在别人的机器上完成,而唯一能告诉你执行结果的那份回执,默认是关着的,得你主动去开。
平台侧的告警同样要主动去订阅,三个平台后台的告警怎么分级诊断整理了各家通知的开关位置。
浏览器这边是报告端点,域名那边是发信策略的聚合报告地址,广告平台那边是变更历史和政策通知。它们的共同点不是难配,是不配也不报错。系统照常运转,报表照常出,只有当你需要复盘“上周三那次改动到底影响了什么”的时候,才发现那段时间是一片空白。
那79个完整性校验是谁加上去的?
子资源完整性是另一条独立的防线:在script标签上写一个integrity属性,值是这个文件内容的哈希。浏览器下载完先算一遍,对不上就不执行。它防的正是白名单防不住的那一类——域名没变,文件内容被换了。
页面上还有一批平台代填的结构化字段,结构化数据审计工具怎么用能一次扒清五种格式里各自缺了什么。
39个站有,其中38个恰好是2个
56个页面上一共4133个script标签,带integrity的79个,占1.91%。分布很有意思:
要自己动手算这类哈希,哈希生成工具怎么用把算法选择和完整性校验的取值格式讲清楚了。
| 情况 | 站数 |
|---|---|
| 页面上恰好2个脚本带完整性校验 | 38 |
| 恰好3个 | 1 |
| 一个都没有 | 17 |
这38个站的脚本总数从57到161不等,差别很大,但带哈希的永远是2个。2这个数字在38个互不相干的品牌身上一模一样,那它就不是这些品牌加的。对着页面源码翻一下就能确认:那两个标签指向的是建站平台自己的运行时脚本,哈希是平台在渲染页面时一起写进去的。
SRI和CSP来自同一个模板
把两组数据叠在一起看,对应关系严丝合缝:
主题决定了你继承到什么,主题怎么选才不给自己挖坑列了从代码质量到构建方式的判断项。
| 这个站的CSP | 站数 | 页面上有几个integrity |
|---|---|---|
| 出厂式的70或75字节版本 | 39 | 2个或3个 |
| 其他短策略(49、71、175字节) | 3 | 0个 |
| 自己写的长策略 | 5 | 0个 |
| 没有策略 | 9 | 0个 |
那两个哈希和那三条指令来自同一个出厂模板;谁把模板换掉了,两样一起没了。这就是“代填”这件事最隐蔽的成本:你不知道自己继承过什么,也就不知道自己什么时候把它丢了。
那5个花了大力气写出几千字节白名单的站,页面上一个integrity都没有。他们把默认主题换掉了,自建了前端,于是平台那两个自动带哈希的脚本也一起没了,而自己的构建流程里没有把哈希这一步加回来。他们在一件事上做得比所有人都认真,同时在另一件事上退回到了零。
那两个哈希指向的是什么
顺手把那两个带哈希的标签翻出来看,指向的是平台运行时的两个脚本文件,地址在平台自己的内容分发域名下,文件名里带着构建版本号。哈希用的是较强的那一档摘要算法,写法完全规范。
这说明平台不是不懂这件事,是只对自己负责的那部分做了。这个态度其实完全合理:平台能保证自己那两个文件的完整性,但没法替你保证你装的那十几个应用。责任边界画得很清楚。
问题在于这条边界对商家是隐形的。页面源码里那两个漂漂亮亮的完整性属性,会让任何一个不细看的人以为“我们这一页是做过完整性校验的”。它确实做过,只是覆盖了两个脚本,而页面上有八十个。这也是为什么前面建议用数数量而不是看有没有——数出来是2,你就知道它的真实覆盖范围。
为什么这条线这么难铺
完整性校验有个天生的麻烦:第三方脚本经常更新,哈希一变浏览器就拒绝执行,页面上那块功能当场消失。所以对高频更新的营销类SDK,直接加integrity是不现实的。规范本身也提醒,跨域加载的资源还需要对方配合返回允许跨域读取的响应头,否则浏览器连算哈希的机会都没有。
把第三方脚本代理到自己域名下是常见解法,边缘缓存的回源与缓存键实战说明了这样做要付的运维成本。
现实的做法分两档。能锁版本的锁版本:把第三方脚本固定到带版本号的具体文件上,加哈希,升级走发版流程。锁不了版本的走另一条路:不追求阻断,改成监测——定期取一次那个脚本的内容,算哈希,跟上次比,变了就告警,由人判断这次变更是不是预期内的。这两条路对应的正好是下一节要说的两条要求。
PCI DSS 6.4.3和11.6.1要你交出什么?
把上面这些实测数字放到合规口径下看,性质就变了。这不再是“做得好不好”的问题,是“过不过得了年度评估”的问题。
另一套要同时满足的合规要求在隐私侧,同意横幅怎么配才不毁数据讲了两套要求怎么共存。
支付卡行业数据安全标准第4版里有两条专门针对支付页脚本的要求,从2025年3月31日起从“最佳实践”变成硬要求。标准委员会为这两条专门出过一份防范页面脚本窃取的指引补充文件,把适用范围和常见误解讲得比标准正文细得多。
6.4.3要的是一份清单
这一条要求支付页上加载的每一个脚本都满足三件事:经过授权、有完整性保证的手段、有书面理由说明为什么需要它。三件事合起来就是一份清单——哪些脚本可以出现在这一页上,各自为什么在这儿,各自靠什么保证没被改。
清单类工作最怕没有框架,企业网站审计到底该查什么那份诊断框架的组织方式可以直接套过来。
对照实测:页面上第三方域名中位数7个,完整性保证的覆盖率1.91%,而那1.91%还不是商户自己加的。这一条的现状基本是空白。
11.6.1要的是一个会响的机制
这一条要求部署一套变更与篡改检测机制,在支付页的内容或者影响安全的响应头被未授权修改时告警,执行频率至少每7天一次,风险评估认为需要更频繁的就更频繁。
机制配了不等于机制能用,备份了就安全吗恢复演练才是真底气讲的正是没演练过的机制会怎么失效。
注意它把响应头和页面内容并列了。这意味着“有人悄悄把安全策略那一行删了”本身就是这条要求要抓的事件。回想前面那9个拿到旧模板的站——如果平台哪天把模板又改一次,他们同样不会知道。
两条是一对,不是两件事
6.4.3定的是基线:这些脚本是被批准的。11.6.1盯的是偏离:有东西跟基线不一样了。只做前者,你有一份三个月前的文档;只做后者,你有一堆没有参照物的告警。这两条必须一起落地才有意义,这也是它们在评估时经常被一起判不通过的原因。
基线加偏离检测这个组合到处都能用,自动内链插件翻车后改回手动布链是同一个思路在另一件事上的落地。
自评问卷选哪一份,答案在这一页上
很多团队第一次接触这两条要求,是在填自评问卷的时候被卡住的。问卷分好几种,选错哪一份,后面几十道题全部白填。
简单说,如果你的支付流程完全托管在服务商那边、你的页面上不含任何能影响那一屏的元素,适用的是最轻的那一档;一旦你的页面上有自己的脚本、有标签容器、有能改结构的第三方组件,就要往上跳一档,而这一档恰恰把脚本清单和变更检测写成了必答题。
判断的关键就在这一页上。打开购物车页数一遍第三方脚本域名,如果不是零,基本可以确定你不在最轻的那一档里。这批样本里56个站有48个第三方域名不为零,比例是85.7%。
更麻烦的是这个判断会随时间漂移。今天你的页面很干净,符合最轻那一档;三个月后运营装了一个评价组件,档位就变了,而没有人会因为装了一个应用去重新做一次范围判定。把“第三方脚本数从零变成非零”做成一条告警,比每年做一次范围复审有效得多。这条告警的实现成本几乎为零——比对脚本已经在跑了,只是多加一个判断。
还有一个容易忽略的方向:范围也可以往回缩。有团队把评价组件、聊天窗口、推荐位统一从购物车页移到商品页和首页之后,购物车页的第三方域名从九个降到零,第二年的评估直接换了一份更轻的问卷。这不是钻空子,是把风险和收益放在了各自该在的位置——评价和推荐的收益本来就发生在决策阶段,不发生在结账阶段。
范围问题比技术问题更容易踩坑
很多团队的第一反应是“我们用的是托管支付,卡号不落我们的服务器,跟我们没关系”。这个判断在纯iframe嵌入的场景下部分成立,但边界比想象的窄:只要你的页面能影响那个iframe的加载——比如那一页上有你自己的脚本、有标签容器、有能改DOM的第三方组件——这一页就进了范围。
支付合规的范围判定还牵扯到风控规则,新规上线后拒付率怎么降一半从另一个角度补齐了这条线。
判据很朴素:如果有人能通过改这一页的代码,把用户引到一个假的支付框上,那这一页就是支付页。按这个判据回看那56个站,第三方域名中位数7个的购物车页,几乎没有一个能干净地划到范围之外。
评估时实际会被问到什么
把这两条从条文变成对话,评估人员通常会顺着这么几个问题往下问:
这类问答往往由法务牵头,法务与技术协作的七个动作点整理了双方各自要准备什么材料。
| 问题 | 要拿出什么 | 常见的答不上来的点 |
|---|---|---|
| 支付页有哪些? | 页面清单与范围判定依据 | 只报了结算页,漏了承载它的上一页 |
| 这些页上有哪些脚本? | 脚本清单,含内联 | 只有外部脚本,内联部分是空的 |
| 每个脚本为什么需要? | 书面理由与批准记录 | 理由写成了“历史原因” |
| 你怎么保证它们没被改? | 哈希或监测机制的证据 | 说了有监测,拿不出告警记录 |
| 多久跑一次? | 频率与执行日志 | 配了任务但从没成功跑过 |
| 响应头变了会怎样? | 头部变更的检测方式 | 完全没考虑过这一项 |
最后一行是最容易漏的。大多数团队把注意力全放在脚本上,忘了标准原文把响应头和页面内容并列写在一起。而响应头恰恰是最容易被悄悄改掉的东西——换一次CDN配置、改一次网关规则、平台升级一次模板,都可能让那一行安全策略消失,而页面看起来毫无变化。
怎么把范围真正缩小
能显著缩小范围的做法只有一种:让用户整页跳到支付服务商的域名上完成输入,而不是在自己页面里嵌一个框。这样你的域名上就不存在“能影响卡号输入”的页面了。
整页跳转还是嵌框,取决于网关怎么接,支付网关怎么配才能顺利收款对比了几种接法的差别。
代价也很实在:跳出去之后你失去对那一屏的样式控制,跳转本身会掉一部分转化,回跳时还得处理好会话状态。这笔账建议按评估等级和团队规模算,而不是按转化率单点算——一个五人的独立站团队要长期维持一份活着的脚本清单,实际成本比想象中高得多。
出厂默认为什么总是偏向放行?
说到这儿,很容易滑向“平台不负责任”的结论。这个结论不成立,值得掰开看。
服务器这一层的默认值同样值得整体过一遍,服务器配置影响的二十项清单可以当成一次性的对照表。
平台面对的约束和你不一样
建站平台要让几十万个商家、几万个应用、无数个主题都开箱即用。如果默认策略里带一个严格的script-src,那么每装一个新应用,商家的页面就有一块功能失效,而绝大多数商家看不懂控制台里的红字。对平台来说,默认收紧的成本是海量的支持工单,默认放行的成本是分散在个别商家身上的、不会归因到平台的风险。
平台的取舍逻辑决定了你会继承什么,做独立站到底用什么建站把几条路线的默认约束摆在了一起。
所以出厂默认的方向几乎必然是放行。这不是道德问题,是激励问题。凡是“出错的人不是定这个默认值的人”,默认值就一定偏向不出错的那一边,而“不出错”在平台口径下的意思是“不产生工单”。
反过来的例子:也有偏向收紧的默认值
为了不把这个判断说成一条万能定律,得给个反例。HSTS这条响应头的覆盖率是98.2%,它的作用是强制浏览器只用加密协议访问你的站,一旦开启并且带上较长的有效期,回退到明文协议这件事就做不到了。这是一条收紧的默认值,而且是一条一旦开错很难撤销的默认值。
另一组一开就很难撤的默认值是缓存策略,浏览器缓存头怎么配才不犯改了不更新的事故给了取值建议。
为什么平台敢默认给这条?因为它的失败模式对商家是不可见的:开了HSTS之后,用户不会看到任何异常,只有那些还在用明文地址的老链接会被自动升级。它不会产生工单。
所以规律的准确表述不是“默认值总是放行”,而是“默认值总是选那个不产生工单的方向”。这个方向大多数时候等于放行,但当收紧不产生工单时,它也会选收紧。判断一个默认值会往哪边倒,看的不是安全收益,是出错时谁先收到投诉。
用另外两条响应头验证这个判断
如果上面的解释成立,那么应该能观察到这样一个现象:同一批站上,凡是平台默认给的安全头覆盖率就高,凡是需要商家自己配的就低,而且高低跟这条头有多重要没有关系。
响应头这一层能做的事比多数人以为的多,响应头的机制与实战把几类常用头的作用域讲清楚了。
| 响应头 | 覆盖站数 | 覆盖率 | 谁给的 |
|---|---|---|---|
| Strict-Transport-Security | 55 | 98.2% | 平台或CDN默认 |
| X-Content-Type-Options | 52 | 92.9% | 平台默认 |
| Content-Security-Policy | 47 | 83.9% | 平台默认的两三条 |
| X-Frame-Options | 36 | 64.3% | 部分模板默认 |
| Referrer-Policy | 2 | 3.6% | 要自己配 |
| Permissions-Policy | 2 | 3.6% | 要自己配 |
断崖出现在第四行和第五行之间:从64.3%直接掉到3.6%。而Referrer-Policy对一个电商站的意义一点都不小,它决定用户从你的页面跳到第三方时,地址栏里那串可能带着订单号、优惠码甚至邮箱的查询参数,会不会被原样带过去。这条响应头的取值直接决定了外泄的粒度,从完整地址到只给域名再到什么都不给,中间有好几档。
配了这两条的两个站是谁?一个是自己写了一万两千多字节策略的个护品牌,另一个是把整套前端自建了的配件品牌。覆盖率表面上是56个站的分布,实际上只反映两件事:平台填了什么,以及那两个自己动手的人填了什么。
代填的东西出了事算谁的
评估的时候没有人会问“这几条指令是谁写的”,问的是“你的支付页上跑了哪些脚本、各自为什么在这儿、你怎么知道它们没被改”。代填的那一格出了问题,责任在你,不在填它的人。
责任边界这件事在别的场景也一样,海外营销哪些内容绝对不能碰列了五类红线与发布前的自检动作。
这个规则听上去不公平,但它其实是唯一可执行的规则。平台不知道你这一页在卖什么、装了哪些应用、哪些第三方是你谈的商务合作;只有你知道。责任跟着知情程度走,不跟着代码作者走。
一个真实的时间线
保哥前年跟一个做户外装备的独立站团队复盘过一次事故,过程值得原样复述。他们上了一个做加购推荐的应用,装完当天转化率确实涨了。三个月后,那个应用的服务商换了CDN,脚本地址从自有域名改到了一个通用对象存储的域名上。
那类加购推荐组件本身也值得单独评估,购物车里推错一次配件用户就不再相信任何推荐位算过它的隐性成本。
结果是这样的:页面照常工作,因为他们的策略里根本没有script-src,新域名不受任何限制就加载了;他们自己毫不知情,因为没有报告端点也没有变更检测;直到季度评估时对方问“这个存储桶的域名是谁的”,团队里没有一个人能回答。那次的整改动作不是加防护,是花了两周把这一页上的脚本一个个认领了一遍,认到最后有三个谁也说不出是什么时候装的。
这个案例里最贵的不是风险,是“认领”这两个字花掉的两周。基线的价值不在于它能拦住什么,在于它让“这是不是新东西”变成一个五秒钟就能回答的问题。
30秒怎么判断自己是不是那42个之一?
不需要工具,不需要登录,一条命令。
浏览器扩展也能十秒看清一个站的技术栈,十款技术栈检测扩展实测对比挑出了几个查响应头最顺手的。
curl -sI https://你的域名/cart | grep -i content-security-policy把回来的那一行拿去看三件事,顺序不要换。
第一件:这一行有多长
如果整行加起来不到两百个字节,基本可以确定是出厂值。真写过的策略不可能这么短,因为光把常用的几个第三方域名列一遍就不止这个长度。这批样本里出厂式的全在175字节以内,自己写的最短也有4469字节,中间是真空。
不想敲命令行的话,接口测试工具怎么用也能一次看清一个地址的状态码与全部响应头。
第二件:里面有没有script-src这十个字符
没有就是没有约束脚本来源。这一条不用理解语义,用眼睛找字符串就行。顺带看一眼有没有default-src,写了default-src没写script-src的也算数。
页面层的其他项可以顺手一起查,页面标签检测器怎么用用十项加权评分给整页做一次体检。
第三件:有没有report-uri或者report-to
没有的话,这条策略就是只做不说。哪怕它拦对了一百次,你也一次都不会知道。
平台侧还有一批默认关着的报告,生成式报告与屏蔽开关怎么读说明了哪些数据要主动打开才有。
再补一条十秒的动作,用来看完整性校验:
curl -s https://你的域名/cart | grep -o 'integrity=' | wc -l结果如果正好是2,那多半是平台自动加的那两个;如果是0,那连平台那两个都没有,通常意味着主题被换过;如果明显大于3,恭喜,你们真的做过这件事。
把这三条做成一次性的自查表
| 看什么 | 结果 | 说明 |
|---|---|---|
| 策略长度 | 小于200字节 | 出厂值,没人动过 |
| 策略长度 | 大于4000字节 | 有人认真写过 |
| 有没有script-src | 没有 | 脚本来源不设限 |
| 有没有report端点 | 没有 | 拦了什么你不会知道 |
| integrity数量 | 恰好2个 | 平台加的,不是你加的 |
| integrity数量 | 0个 | 主题换过,连平台那两个也没了 |
这类自查完全不用买工具,预算为零也能做好的那份工具清单里有可以直接拿来跑的免费选项。
脚本清单基线怎么落地才不变成一次性文档?
清单这个东西的失败模式非常固定:做的时候很认真,做完存进共享盘,三个月后没人记得它在哪儿。要让它活着,得让它每次发版都被读一次。
这件事最终要落在前端的日常流程里,前端与搜索侧协作的七个动作点给了把检查嵌进发版的具体位置。
清单里放什么
最小可用的一行记录只需要五列,多了没人维护:
留改并删的决策结构在内容侧早有成熟版本,上千篇旧内容怎么做留改并删转的决策可以直接借表结构。
| 列 | 内容 | 为什么需要 |
|---|---|---|
| 域名 | 脚本加载的完整主机名 | 比对的主键 |
| 归属 | 哪家服务商、哪个应用 | 出事时找谁 |
| 用途 | 一句话说清为什么需要 | 6.4.3要的书面理由 |
| 负责人 | 业务侧一个具体的人 | 决定要不要留 |
| 保护方式 | 锁哈希还是只监测 | 决定告警怎么处理 |
第四列最容易被跳过,也最重要。技术侧永远无法独立判断一个营销组件该不该留在支付页上,这个判断只能由当初决定装它的人来做。清单里没有这一列,比对出新增之后就会卡在“这个要问谁”上,一卡就是两周。
第一版怎么在两小时内做出来
不要一开始就追求完整。第一版的目标只有一个:把“现在页面上有什么”变成一份可以打开的文件。
跨岗位认领这件事最容易卡住,数据分析师与技术对账的七个动作点给了一套让对方愿意回信的问法。
做法很直白。抓一次购物车页,把所有带src的脚本域名去重列出来,填进表格的第一列。这一步十分钟。然后按域名去搜服务商名字,填第二列,这一步大概半小时,因为大部分域名一搜就知道是谁。第三列用途先按服务商类型粗填,比如“评价组件”“客服”“推荐”。第四列负责人留空,等下一步。
接着把这份表发给运营、投放、客服三个岗位各一份,只问一个问题:这里面哪些是你装的。认领完之后第四列就有了,认领不出来的那几行单独标出来,它们才是真正需要花时间查的。这一步通常两三天出结果,但你的手上活儿到这里就结束了。
保哥带一个做母婴用品的团队做这件事时,第一版表格上有19行,两天之后有4行没人认领。查下去发现两个是三年前的代理商装的追踪代码,一个是早就停用的调查问卷工具,还有一个是主题作者塞在模板里的推广脚本。这四行删掉之后,页面首屏渲染时间少了将近四百毫秒——安全动作意外地变成了性能动作,这在做清单时非常常见。
比对怎么自动化
思路很朴素:抓一次页面,抽出所有脚本域名,跟清单做差集,有新增就告警。跑在发版流水线里,也跑在定时任务里,因为第三方换CDN这种事不会挑你发版的日子。
比对脚本挂上定时任务就成了常态化机制,用定时任务把独立站运维自动化有现成的调度与告警写法。
频率的下限是每7天一次。实务上建议两档:发版后立刻跑一次,抓自己人引入的变化;每天跑一次,抓第三方引入的变化。后者更重要,因为前者至少有人知道改过东西。
内联脚本怎么进清单
外部脚本按域名管,内联脚本没有域名,管法不一样。可行的做法是把每个内联块的内容算一个哈希,把哈希清单存起来,比对时看有没有出现新的哈希。
定时表达式不用背,定时表达式生成器怎么用可以直接生成每天一次和发版后一次这两条规则。
这个做法的麻烦在于误报率:内联块里经常带着随机的nonce、时间戳、购物车token,每次请求都不一样,哈希天天变。解法是在算哈希之前先做一次归一化,把已知的变动片段用占位符替换掉,剩下的结构再算哈希。这一步一次性投入不小,但做完之后内联脚本的变更就跟外部脚本一样可比对了。
同意状态会让同一页有好几副面孔
这一条值得单独展开,因为它直接决定你的清单准不准。
同意管理平台怎么选直接决定了这件事有多难,四家主流同意管理平台横评与选型决策树可以先看一遍。
装了同意管理平台的站,页面上的脚本集合是随用户选择变化的。用户全部拒绝时,只加载严格必要的那几个;用户全部接受时,营销和分析类的脚本才被注入。这意味着同一个地址至少有两副面孔,而抓取脚本默认拿到的是最干净的那一副。
这批数据里有10个站挂了同意管理组件,它们的第三方脚本数很可能被系统性低估了。要补齐,得在抓取时带上一个模拟“全部接受”的Cookie再跑一遍,然后把两次的差集单独列出来——那个差集正是营销侧真正装了什么的清单。
顺带一提,这个差集在合规上的地位很微妙:那些只在用户同意后才加载的脚本,同样会出现在支付相关页面上,同样要进清单。“用户同意了”解决的是隐私法的问题,不解决支付标准的问题,这是两套互不替代的要求。
告警该发给谁
比对跑起来之后的第一个真问题不是技术,是通知路径。新增一个陌生域名这件事,技术侧收到之后一样答不出来,转手就会变成一条挂在群里的消息,然后沉下去。
跨岗位的通知路径要提前约定好,内容运营与技术协作的七个动作点给了一份可以照抄的责任划分。
可行的做法是按清单里的第四列路由:新增域名如果能匹配上某个已知服务商,就直接通知那个服务商的对接人;匹配不上的才升级到安全侧。这样绝大多数告警会落到当初装它的人手里,而那个人五秒钟就能判断这次变更是不是预期内的。
还有一个细节值得抄:告警正文里带上“上一次这个域名出现是什么时候”。第三方换CDN、灰度发布、地区分流都会让某个域名时有时无,带上历史之后,收到通知的人能立刻分辨这是新东西还是老熟人。没有这一行,同一个域名会反复触发告警,两周之后所有人都会把这个通道静音。
三个常见的坑
第一个坑是把清单做成完整主机名的白名单,然后发现平台自己的CDN会带随机子域,比对天天报新增。解法是按域名后缀匹配,别按完整主机名。
同一个地址对不同来访者返回不同内容,这件事本身要先量出来,渲染对比器怎么用就是干这个的。
第二个坑是只抓了首页没抓购物车页。这两页的脚本集合差别很大,营销组件通常只在购物车页出现。这批样本里就有站的首页第三方域名比购物车页少一半。
第三个坑最隐蔽:抓页面时用了不带Cookie的干净请求,而真实用户带着同意状态。同意管理平台会根据用户的选择动态加载不同的脚本,你在服务端抓到的可能只是“全部拒绝”那一版。这一条会让你的清单系统性地偏小,得用带上典型同意状态的请求再跑一遍补齐。这批数据同样受这个限制影响——所以文中那些数字是下限,不是上限。
做完这一轮之后,下一步该看哪儿
把购物车页的清单做完,惯性会让人接着去做首页和商品页。这个顺序不一定对。
权限对齐这件事在工程侧有成熟做法,安全评审到权限与提示注入防御实战里的分级授权模型可以搬到运营侧。
更有价值的下一步是往回走一格,去看那些能改这一页的入口:标签容器有几个人有编辑权限、主题的代码谁能改、应用的安装权限在谁手上、CDN的规则谁能动。页面上的脚本是结果,这四个入口是原因,堵住入口比反复清理结果便宜得多。
实务上最常见的失衡是:代码仓库有严格的评审流程,而标签容器的编辑权限散在五六个人手里,没有评审也没有日志。两条路通向同一个页面,一条走得比另一条严格十倍,攻击者当然走松的那条。做完清单之后,花半天把权限对齐,收益通常比再清理两页脚本高。
这条判据还能搬到哪儿
把这次的结论抽出来,是一句跟支付没关系的话:你没填的那一格不会空着,系统会替你填一个具体的、正在生效的值;它长得像你的决定,出问题时算你的责任,但作者不是你,改动也不通知你。
换个地方用之前先校准尺子,第三方工具的数据到底准不准给了六步校准法,省得被噪声带偏。
代填有三种形态,这篇讲的是第一种:
| 形态 | 值从哪儿来 | 什么时候变 | 怎么粗筛 |
|---|---|---|---|
| 出厂代填 | 开户那天就有的默认配置 | 平台改模板时 | 看这一格是不是跟别人一模一样 |
| 上游代填 | 你引用的那几家现给 | 他们随时能改 | 看这个值展开之后有多少是别人说了算的 |
| 现抓代填 | 用的时候从你页面上抓 | 每次调用都可能不同 | 看它抓的时候你这一页上摆着什么 |
这三个问题问的都不是“配得对不对”,而是“这一格是谁填的”。换个位置就能复用,成本是一条命令。
顺着这条判据往外走一步,会发现最值得先查的不是最重要的那一格,而是最没人碰过的那一格。重要的格子通常有人负责,出了事有人认领;没人碰过的格子往往连负责人都没有,出了事第一反应是互相打听“这个是谁配的”。这批数据里那42条一模一样的策略,最贵的不是它们不够严,是它们背后一个人都没有。
所以这件事真正的产出物不是一条更长的策略,是一份写着人名的表。策略可以照抄,表不行。
最后留一个可以立刻验证的动作:打开你自己的购物车页,把响应头里那一行策略复制出来,数一下它有多少个字节。如果这个数在两百以内,那么恭喜,你刚刚用十秒钟确认了这一页上最重要的一条安全规则不是你写的,也从来没有人为它负过责。接下来要做的第一件事不是改它,是找到那个应该为它负责的人。这个人找到了,后面所有动作都会变得简单:写策略、列清单、接告警,都是他一句话就能拍板的事;找不到这个人,再详细的技术方案也只会在群里躺三个月。
常见问题解答
购物车页和真正的支付页是一回事吗?
不是同一个页面,但在合规范围上经常是同一件事。判据不是页面叫什么名字,而是这一页上的代码有没有能力影响用户输入卡号那一屏。购物车页上如果有能改DOM的第三方脚本,它就能把结算按钮指向别处,因此通常要一起管起来。这次测购物车页而不是结算页,是因为结算页大多需要真实加购和会话,无法用一次匿名请求公平地横向比较。
下单之后那几页同样在你的域名上,物流跟踪页把用户一脚踢到第三方站上说明了它们同样需要被盘进来。
那9个拿到旧模板的站,能自己升级到新模板吗?
通常不能,也不需要等。默认模板由平台按开店时间下发,商家后台没有“切换到新版默认安全头”这个开关。但这件事完全可以自己补:在主题或者边缘层加一个响应头,把那两条补上,成本是分钟级的。补的时候注意别和平台已有的那条冲突——同一个响应头出现两次,浏览器会按最严的那份执行,结果可能比你预期的更严。
在服务器层补响应头有现成的写法,配置文件的六层综合治理把重写、缓存、规范化和加密强制放在了一起。
用了托管支付或者纯iframe嵌入,是不是就不用管这两条要求了?
范围会小很多,但不等于没有。只要承载iframe的那一页上跑着你控制之外的脚本,它就能替换iframe的地址,或者在旁边画一个足以乱真的假输入框。真正能大幅缩小范围的做法是让用户整页跳转到支付服务商的域名,而不是在自己页面里嵌一个框。
加了integrity之后第三方脚本更新导致页面出错,怎么办?
分两类处理。版本可控的第三方,把地址固定到带版本号的文件上,哈希跟着版本走,升级走正常发版流程。版本不可控、由服务商随时推送的,别加哈希,改成定期取内容算哈希做比对告警,用监测满足要求。硬给高频更新的营销SDK加哈希,结果一定是某天页面上那块功能突然消失,而且没人知道为什么。
把小体积的第三方资源内联进来是另一条思路,内联编码与性能权衡算过什么尺寸以下值得这么干。
只把策略加严,不做清单,行不行?
短期能挡住一部分风险,长期不行。策略里的白名单本身就是一份清单,只是写成了机器读的格式,缺了归属、理由和负责人。等到某天要删掉一个域名时,没有人敢按删除键,因为不知道那是谁在用。清单和策略是同一件事的两种表达,缺了人读的那一版,机器读的那一版就会只增不减。
机器可读和人可读两份文档要同步维护,这个道理在别处也一样,内链架构怎么搭是同一种双份维护的样本。
怎么判断某个第三方脚本是不是真的需要留在这一页上?
问三个问题:这个组件带来的收益是发生在这一页上的吗?把它挪到上一页或者下一页,用户体验会差多少?它需要读表单里的输入吗?第三个问题的答案如果是需要,那这个组件就得走最严的流程。实务上,购物车页上的评价组件、社交分享、实验脚本,大多数都能往前挪一页。
推荐位到底带来多少增量,本身就是笔糊涂账,两套报表算出相反的结论而且都没算错拆过这个口径问题。
体检工具报的“已配置内容安全策略”能不能直接当结论?
不能。绝大多数工具报的是这个响应头存不存在,不看里面写了什么。用这次的数据说话:同一批站,按“有没有这一行”是83.9%,按“这一行管不管脚本”是8.9%。看到工具报绿灯时,把那一行原文拿出来自己扫一眼script-src,成本五秒,结论完全不同。
把审计交给工具或者模型之前,有三个前提要先满足,数据、方法与人工复核这三关说明了哪一关最容易漏。
换一个建站平台能解决这个问题吗?
不能,只会换一组默认值。这批样本里那些不用主流建站平台的站,同样有自己的出厂形态:有的是CDN统一下发的一套响应头,有的是前端框架脚手架自带的模板,有的是运维在网关上配的一行通用规则。只要中间有一层不是你写的,那一层就会替你填格子。区别只在于填的内容不同,以及你去哪儿改它。
换平台会换来另一组默认值和另一组坑,为什么开箱就慢就是另一套技术栈上的同类问题。
真正的解法不是挑平台,是把“这一格是谁填的”变成一个每次发版都会被问一次的问题。问题问出来了,用哪个平台都能答。
安全策略写严了会不会影响转化率?
直接影响很小,间接影响要看你怎么上线。策略本身不增加任何请求,也不改变渲染时机,写得再长也就是响应头多几千字节。真正会伤转化的是上线方式:不做观察期直接切拦截模式,某个营销组件被拦掉了,页面上那块空白可能正好是加购按钮旁边的库存提示。
真正影响转化的还是加载体验本身,低端机与弱网下的移动端性能优化量过每多一秒会掉多少人。
所以顺序很重要:先仅报告模式,再白名单,再拦截。这三步走完通常要三到四周,中间任何一步发现有争议的域名,都应该停下来先问业务而不是先决定拦不拦。
这批数据能代表整个行业吗?
不能,它有三个明确的偏差。样本偏向北美中大型DTC品牌,其中用同一个建站平台的占比很高,所以“出厂式策略”的比例被这个平台的默认值主导了;被防护挡下的那17个站没有进入统计,而它们的安全投入很可能高于平均;抓取用的是不带同意状态的匿名请求,脚本数是下限。这批数字的价值不在于精确的百分比,在于那个断崖式的长度分布——那个形状不会因为换一批样本就消失。
抽样设计决定了结论的适用边界,排名追踪要不要每天扫那篇里的样本量与成本权衡同样适用于这类普查。
权威参考资料
本文标题:《支付页CSP实测53个站:45个配了,只有5个管脚本》
本文链接:https://zhangwenbao.com/payment-page-csp-factory-default-script-policy.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0
← 上一篇
robots规则的星号对AdsBot无效,广告落地页照抓下一篇 →
没有了