照着加固清单加的一行响应头,把嵌进来的地图和支付按钮一起变成了摆设
本文目录
- 加固上线当天,页面上会先有哪几样东西没反应?
- 四类能力的失败姿势各不相同
- 摄像头那个错误名,跟用户点“拒绝”是同一个
- 服务端这一侧完全是空的
- 不发这行头的时候,跨源iframe里的摄像头本来是开还是关?
- 基线组:什么都不写,第三方组件也拿不到
- 那为什么现网那些组件用得好好的?
- 把默认值原样写进去,为什么结果就变了?
- 为什么会这样
- 最后一行也值得单看:()连你自己都关
- 响应头和iframe的allow属性,为什么必须两边都对?
- 责任方和补救点不在一个人手里
- 同一个第三方服务,为什么脚本接入是活的、iframe接入是死的?
- 两种接入方式的实测结果
- 这条对选型的实际影响
- 这行头写错了,浏览器会告诉你吗?
- 前三种是同一个原因
- 后三种更有意思
- 为什么换个浏览器怎么试都是正常的?
- 阳性锚点让这个结论站得住
- 两个方向的错都会犯
- 组件哑掉了,能不能提前收到通知?
- 四组实验的结果
- 阳性锚点:同一个端点,另一种机制秒到
- 那个“先观察一周再决定”的标准动作
- 一个必须排除的干扰项
- 149个站是怎么写这行头的,你该照着改什么?
- 覆盖率和取值分布
- 最扎眼的一条:15个站的取值一个字符都不差
- 这份模板里有三项特别值得单拎出来
- 两个现网活样本
- 还留着的配置化石
- 该怎么改
- 常见问题解答
- 不配置这行响应头,是不是就等于把所有能力都开放了?
- 为什么我把摄像头写成默认值以后,嵌入的组件反而用不了了?
- 组件被策略挡住的时候,前端代码能不能捕获到?
- 写错了这行头,浏览器会不会报错?
- 为什么同一份配置在不同浏览器上表现不一样?
- 先上Report-Only观察一段时间,是不是稳妥做法?
- 第三方组件从脚本标签换成iframe,会有什么副作用?
- 权威参考资料
摘要:这行响应头最反直觉的一点是,把某项能力按照它本来的默认值原样写进去,和一个字都不写,结果不一样。实验台上的单变量对照摆在那儿:头里根本不提摄像头,嵌进来的第三方组件用得了摄像头;把
camera=(self)写上去——self正是它文档里写明的默认值——同一个组件立刻拿不到。149个现网站点里有28个发了这行头,其中15个站的取值一个字符都不差,横跨DTC品牌、Magento商城和技术问答社区。而在另一个主流浏览器内核上,这行头压根不执行。
先把场景摆出来。页面上嵌着一个第三方的门店地图,用户点“查找附近门店”,地图请求一次定位,把最近的几家标出来——这条路径是本地搜索转化里最短的那一段。功能上线之后一直没出过事。某天安全同事按一份加固清单给全站补了几行响应头,当天下午客服工单里开始出现同一句话:按钮点了没反应。
接下来的排查会依次撞上几堵墙。服务器访问日志干净,那次定位请求根本没发出去过;第三方组件方回复说他们那边一切正常,也确实正常;前端同事在本地跑一遍,好好的——因为本地开发服务器不走生产那份配置。整条链路上没有一个人手里有能证明出事的证据。
罪魁是Permissions-Policy。它是一行HTTP响应头,管的不是这次请求怎么处理,而是这个页面以及页面里嵌的东西被允许调用哪些浏览器能力:摄像头、麦克风、定位、支付、剪贴板、传感器。它跟那些管索引和缓存的响应头不是一路货色,后者影响的是搜索引擎怎么看你,这一行影响的是浏览器让不让你的代码干活。
写法看上去很朴素:
Permissions-Policy: geolocation=(), camera=(self), payment=*三种取值:()是谁都不许,(self)是只有本站,*是所有人。看着像三档开关,简单到不值得写一篇文章。麻烦恰恰在这儿——它的行为跟这三个词的字面意思对不上,而且对不上的方式非常安静。
下面的结论不是读文档读出来的。保哥搭了一套双主机名实验台:一台当自家站点,另一台当第三方组件的宿主,两边都是真实的HTTPS服务器、真实的网络栈,页面里跑一组探针去调定位、摄像头、麦克风和支付接口,把每次调用落到哪个回调、抛的是哪个错误名全部记下来。加上一轮149个现网站点的普查,一共跑了三十多组对照。每一组都配了阳性锚点,锚点不成立的那组数据一律作废。
加固上线当天,页面上会先有哪几样东西没反应?
先看故障长什么样,因为它长得跟别的故障都不像。
四类能力的失败姿势各不相同
实验台上把四种最常用的能力挨个调一遍,被策略挡住时的表现是这样的:
| 能力 | 被挡住时发生什么 | 代码里能不能接住 |
|---|---|---|
| 定位 | 错误回调被调用,错误码是1,也就是“权限被拒绝” | 只有写了第二个回调才接得住 |
| 摄像头 | Promise被拒,错误名NotAllowedError | 写了catch就接得住 |
| 麦克风 | 同上 | 同上 |
| 支付 | 构造函数直接抛SecurityError | 要用try包起来才接得住 |
定位那一行是最要命的。getCurrentPosition的第二个参数是错误回调,它在语法上是可选的,大量教程里的示例代码只写第一个参数。策略一旦把定位关掉,这类代码的表现就是:函数调用返回了,什么都没发生,也没有任何异常。用户看到的是一个点下去毫无动静的按钮。
摄像头那个错误名,跟用户点“拒绝”是同一个
MDN在摄像头指令那一页写得很清楚:策略挡住这项能力时,getUserMedia()返回的Promise会以NotAllowedError被拒。问题是,用户在权限弹窗上点“拒绝”,抛的也是NotAllowedError。
同一个错误名对应两件完全不同的事:一件是这个用户不想给你,另一件是这个站的运维不让任何人给你。前者是产品问题,后者是配置问题,而代码拿到的信息一模一样。
于是几乎所有集成代码都会走进同一个分支——弹一句“您拒绝了摄像头权限,请在浏览器设置里打开”。用户老老实实去设置里翻一圈,翻不到任何被拒绝的记录,因为他根本没被问过。
服务端这一侧完全是空的
这一条跟嵌入类响应头拦住渲染的那类故障是同一个家族:判决在用户的浏览器里执行,你的服务器和第三方的服务器都不参与。访问日志没有异常,错误率不动,可用性拨测全绿。告警体系里所有指标都是好的,唯一变化的是某个按钮的点击转化率,而那个数字通常没人单独盯。
不发这行头的时候,跨源iframe里的摄像头本来是开还是关?
这是所有误解的起点。很多人默认“我没配就是全开着”,于是加固的时候心里想的是“把不需要的关掉”。实测正好相反。
基线组:什么都不写,第三方组件也拿不到
实验台A组:顶层页面不发任何策略头,页面里嵌一个跨源的iframe,iframe上不写allow属性。结果是四项能力全部被拒——定位错误码1、摄像头和麦克风NotAllowedError、支付SecurityError,控制台四条红字。
原因写在MDN的指令参考页上:
The default allowlist for
cameraisself.
默认名单是self,意思是只有本站自己能用。跨源的iframe不是本站自己,所以默认状态下它就没有这项能力。你什么都不配的时候,第三方组件的摄像头、麦克风、定位本来就是关着的。
那为什么现网那些组件用得好好的?
因为它们的接入代码里带了allow属性。B组把iframe写成allow="geolocation; camera; microphone; payment",其余一个字不改,四项能力全部通过,定位真的返回了坐标。
这就是allow属性的作用:把父页面手里有的能力,委派给这个特定的子框架。你在第三方文档里抄来的那段嵌入代码,末尾那一长串allow="..."不是装饰,是这个组件能不能干活的开关。普查里的证据很直白——149个站首页上的跨源iframe一共40个,写了allow属性的只有8个,而这8个里有5个是视频播放器。
写了
allow的都是“不写立刻看得出坏”的场景,视频播不了谁都看得见。不写的那32个,坏了也没人看得见。
把默认值原样写进去,为什么结果就变了?
这是本文最核心的一组对照,也是最反直觉的一条。为了排除干扰,保哥把这一组做成了严格的单因素隔离:只动一个变量,就是头里那一项写不写、以及写成什么。iframe的allow属性、浏览器、页面代码、权限授予状态全部保持不变。
| 顶层响应头里的摄像头这一项 | 顶层页面自己 | 跨源iframe里的组件 |
|---|---|---|
| 根本不写(头里只有别的指令) | 可用 | 可用 |
camera=(self)——就是它的默认值 | 可用 | 被拒 |
camera=* | 可用 | 可用 |
camera=(self "组件的源") | 可用 | 可用 |
camera=() | 被拒 | 被拒 |
第一行和第二行的差别,是这篇文章存在的理由。文档说摄像头的默认名单就是self;把这个默认值原样敲进响应头,跨源组件从能用变成了不能用。什么都不写和写成默认值,在语义上应该完全等价,在实测里是两个相反的结果。
为什么会这样
MDN那句关于继承的原话给了线索:
For an
<iframe>to have a feature enabled, the origin must be in the allowlist for both the parent page and theallowattribute.
子框架要拿到某项能力,它的源必须同时出现在父页面的名单里和allow属性里。一旦你显式写了camera=(self),父页面的名单里就只有你自己这一个源,第三方组件的源不在里面,allow属性再怎么写也补不上——因为规范里这是个单向开关:
Disabling a feature in a policy is a one-way toggle. If a feature has been disabled for a child frame by its parent frame, the child cannot re-enable it.
而在“没有显式声明”的状态下,浏览器走的是另一条路径,allow属性的委派能力保留着。默认值是一种状态,把默认值写下来是另一种状态,这两件事在这行头里不是一回事。
最后一行也值得单看:()连你自己都关
camera=()那一组,被拒的不只是iframe,顶层页面自己调摄像头也被拒了,控制台原话是“camera is not allowed in this document”。同源的子框架照样被拒。
很多人以为这行头是“管别人的”,写()的时候心里想的是“不让第三方碰”。实际上它是从你自己这一层开始往下全关。如果哪天你自己的页面要加一个扫码功能,这一行会先把你自己拦在门外,而当时写下它的人多半已经不在这个项目上了。
响应头和iframe的allow属性,为什么必须两边都对?
把上面那张表再推一步,就得到这行头最容易出事的结构:两把锁,分别归两个人管,而且锁上了不会响。
| 响应头(运维/后端写在服务器配置里) | allow属性(前端写在HTML里) | 组件能不能用 |
|---|---|---|
| 显式放行组件的源 | 写了 | 能用 |
| 显式放行组件的源 | 没写 | 不能用 |
*——对所有人开放 | 没写 | 不能用 |
*——对所有人开放 | 写了 | 能用 |
写成(self) | 写了 | 不能用 |
第三行是最容易被忽略的:顶层把某项能力对全世界开放,跨源组件依然用不了,只要iframe上没有allow属性。MDN在iframe那一页对这层关系的表述是:
A Permissions Policy specified by the
allowattribute implements a further restriction on top of the policy specified in thePermissions-Policyheader. It doesn't replace it.
是在响应头之上再叠一层限制,不是替代它。所以两边都写对才有用,任何一边留空,结果都是不能用。这跟反向代理那类配置的直觉正好相反:那边少写一条通常是放行,这边少写一条一律是拦住。
责任方和补救点不在一个人手里
这个形状在后端和SEO的协作账本里出现过太多次:出问题的是服务器上一行配置,能补救的却是HTML里一个属性。运维那边看自己的配置,语法正确、清单齐全、扫描器打满分;前端那边看自己的代码,嵌入片段是从第三方文档里原样复制的,一个字没改过。两边各自都对,合起来是坏的。
更麻烦的是,两边用的语法还不一样。响应头里源地址必须加双引号,allow属性里源地址不能加引号。MDN专门为这件事加了一条说明:
the syntax for
<iframe>policies is a bit different to the syntax forPermissions-Policyheaders. The former still uses the same syntax as the older Feature Policy specification.
属性那边沿用的是旧规范的写法,头这边换成了新写法。同一个策略,同一个源,写在两个地方要用两套语法——这大概是唯一一个需要你同时记住新旧两套语法才能配对的响应头。
同一个第三方服务,为什么脚本接入是活的、iframe接入是死的?
大多数第三方服务给你两种接入方式:一段<script>标签,或者一个<iframe>。选哪个通常是按渲染效果和样式隔离来定的,没人会觉得这个选择跟权限有关。实测下来它决定生死。
两种接入方式的实测结果
同一个第三方源,同一行响应头camera=(self),同一个页面里同时用两种方式接进来:
- 脚本方式:第三方的JS从它自己的域加载过来,在宿主页面里执行,探针调摄像头——成功。
- iframe方式:同一个第三方源的页面被嵌进来,同一段探针代码,调摄像头——被拒。
把头换成camera=(),两种方式一起死。MDN那句话解释了这个分裂:
Scripts inherit the policy of their browsing context, regardless of their origin.
脚本继承的是它所在浏览上下文的策略,跟它从哪个域加载来的没关系。跨源脚本一旦跑在你的页面里,它就是“你自己”,享受self这一档待遇。iframe不一样,它有自己的浏览上下文,是被当成外人处理的。
这条对选型的实际影响
一句话:把第三方组件从script换成iframe,是一次安全边界的收紧,也是一次功能的收紧,而这两件事通常没有一起被讨论。
现实里改接入方式的理由五花八门——样式冲突了、第三方要求换、合规要求把第三方脚本隔离开。做这个决定的人在意的是DOM污染和加载性能,没人会想到顺手把这个组件的定位能力也一起收走了。等到几周后有人报“地图不准了”,那次改动早就没人记得了。
这行头写错了,浏览器会告诉你吗?
会说,但说得很小声,而且分三档音量。实验台上把六种典型的写错方式各跑一遍:
| 写法 | 实际后果 | 控制台 |
|---|---|---|
沿用旧语法geolocation 'none'; camera 'none' | 整条头作废,所有能力照常放行 | 一条error |
geolocation=('self')——加了单引号 | 整条头作废 | 一条error |
GEOLOCATION=()——指令名大写 | 整条头作废 | 一条error |
| 源地址没加双引号 | 该项无效,其余指令照常生效 | 一条warning |
| 清单里夹了一个不存在的特性名 | 那一项被忽略,其余指令全部生效 | 一条warning |
camera=self——漏了括号 | 顶层能用、跨源被拒,等于写了(self) | 无 |
前三种是同一个原因
整条作废那三种,控制台文案一模一样:Error with Permissions-Policy header: Parse of permissions policy failed because of errors reported by structured header parser.这不是浏览器的脾气,规范就是这么写的:
Let parsed header be the result of executing get a structured field value... If parsed header is null, return an empty ordered map.
解析不出来就返回一张空表——不是报错,不是退回默认,是当作你什么都没写。所以旧语法那一组的实测结果是“所有能力照常放行”:你以为自己关了六项能力,实际上一项都没关,安全扫描器还会因为响应头里有这个字段给你打勾。
后三种更有意思
不认识的特性名只是被跳过,规范原文是“If feature-name does not identify any recognized policy-controlled feature, then continue”,其余指令照常生效。名字不认识就整项忽略,名字认识但取值写错就整条完蛋——严厉程度跟你想的正好反过来。
最后那一行是最阴的:漏个括号,页面正常、控制台一个字都没有,你自己测怎么试都对,只有跨源组件那一格悄悄从“能用”变成了“不能用”。
整条挂掉是error,单项挂掉是warning,写成语义变化最大的那一种反而一声不吭。配置类隐性失分的老规律在这里又应验了一次:报警级别和实际危害经常是反着的。
为什么换个浏览器怎么试都是正常的?
这一条足以让“我在浏览器上验过了”这句话失去意义。同样15组实验,换一个主流浏览器内核跑一遍:
| 实验组 | 引擎A | 引擎B |
|---|---|---|
不发头,iframe没写allow | 被拒 | 被拒 |
不发头,iframe写了allow | 可用 | 可用 |
头里写(self),iframe写了allow | 被拒 | 可用 |
头里写()全禁,iframe写了allow | 被拒 | 可用 |
| 旧语法/单引号/大写 | 整条作废 | 可用 |
规律很干净:凡是靠iframe的allow属性判决的组,两个引擎结论一致;凡是靠响应头判决的组,引擎B一律放行。这行头在引擎B上根本不执行。
阳性锚点让这个结论站得住
前两行就是锚点。引擎B确实在执行权限策略这套机制——没写allow的跨源iframe照样被拒,说明实验台、探针、权限授予全都正常。它只是不认响应头这一半。MDN在这个头的页面顶部挂了一块牌子:
Limited availability — This feature is not Baseline because it does not work in some of the most widely-used browsers.
“在一些使用最广泛的浏览器上不工作”,说的就是这件事。顺带一提,引擎B里连document.featurePolicy这个自查接口都不存在——你想在页面里问一句“我到底被禁了什么”,在那边连问的地方都没有。
两个方向的错都会犯
这条差异是双向坑:
- 你在引擎B上测组件功能,一切正常,上线后引擎A的用户全线报障;
- 你在引擎A上加固完毕,以为能力都锁死了,引擎B的用户那边这些能力从来没被锁过。
更要命的是这类差异不在渲染层,技术栈检测扩展看不出来,跨浏览器视觉回归测试也拍不出来——两边的页面截图长得一模一样。
组件哑掉了,能不能提前收到通知?
规范给了上报机制,实验台上跑了四组,全部零收获。这一组的价值全在阳性锚点上。
四组实验的结果
- 只发策略头、不给上报端点:0份;
- 策略头 +
Reporting-Endpoints:0份; - 只发Report-Only版本 + 上报端点:0份,而且摄像头是通的;
- 强制版和Report-Only版两条都发 + 上报端点:0份,强制那条说了算。
阳性锚点:同一个端点,另一种机制秒到
零收获这种结果,不配锚点是不能拿来下结论的——万一是接收端写错了呢。所以同一个实验台上加了一页:发一条会被拦的内容安全策略,用的是老式的report-uri上报,接收端点还是同一个地址。
结果是一份格式完整的报文立刻到手,里面连status-code都写着200。通路没问题,接收端没问题,浏览器愿意发报告——只是权限策略的违规一份都没发出来。
而权限策略压根没有report-uri这条老路可退。MDN关于上报的说明里,这类违规的报告类型叫permissions-policy-violation,端点只能通过策略头上的report-to参数、或者Reporting-Endpoints头里那个名叫default的端点来指定。老机制不支持它,新机制在这套环境里一份没发,净结果就是零。
那个“先观察一周再决定”的标准动作
加固上线的标准流程是先挂Report-Only版本,观察一段时间,确认没人被误伤再切强制。实测里Report-Only那一组确实只观察不拦截——摄像头照常能用,这一半是对的。问题是另一半:它观察到的东西一份都没送出来。
一周的Report-Only观察期,收获是一份漂亮的空数据。零告警被读成了零风险,然后强制策略就上线了。
普查数据从另一个方向印证了这件事:149个站里,发Report-Only版本的一个都没有。这个机制既没人用,用了也拿不到东西。
一个必须排除的干扰项
排查时还有一层容易走错:非安全上下文。实验台上把同一个页面挂到不加密的端口上,主机名保持不是本地回环的写法,结果是定位错误码1、摄像头接口直接不存在——症状跟被策略挡住几乎一样。
但这时候页面里的自查接口会告诉你摄像头是允许的。策略层面确实允许,只是卡在了另一道门上。所以排查顺序应该是先确认加密连接、再看策略——顺序反了会在错误的地方挖很久。这也是HTTPS加固那些工作的一项隐性收益:不上加密,这些能力从一开始就没有。
149个站是怎么写这行头的,你该照着改什么?
普查口径:只对首页发一次GET请求,不登录、不提交、不改动任何东西,读响应头和页面上的iframe。抓到149个站。
覆盖率和取值分布
| 指标 | 数量 | 占比 |
|---|---|---|
发了Permissions-Policy | 28 | 18% |
| 发了Report-Only版本 | 0 | 0% |
| 还在发早已改名的旧头 | 3 | —— |
把这行头写进meta标签(无效配置) | 0 | 0% |
| 首页就挂着跨源iframe的站 | 30 | 20% |
最扎眼的一条:15个站的取值一个字符都不差
28个发了这行头的站里,有15个的取值完全相同,逐字符一致,连指令顺序和逗号后没有空格这个细节都一样。这15个站包括几个互不相干的DTC品牌、两三个Magento商城、一个大型零售站,还有一个全球知名的技术问答社区。
accelerometer=(),camera=(),clipboard-read=(),clipboard-write=(),
geolocation=(),gyroscope=(),hid=(),magnetometer=(),microphone=(),
payment=(),publickey-credentials-get=(),screen-wake-lock=(),
serial=(),sync-xhr=(),usb=(),xr-spatial-tracking=*把范围放宽到“特性清单相同但取值可能不同”,是23个站,占28个的82%。这行头在现网基本不是写出来的,是抄出来的。
16项能力全部关死,唯独最后一项空间追踪对全世界开放。没有任何一个站的业务需要这个组合。它之所以在15个毫不相干的站上一模一样,只能是因为没人读过它。
这份模板里有三项特别值得单拎出来
payment=():17个站写了这一项,全部是(),没有一个写别的值。第三方支付按钮大量是以iframe形式接进结账页的,而结账页放弃率那笔账,没人会想到去查一行响应头。publickey-credentials-get=():16个站写了,全部关死。这是通行密钥登录用的接口,关掉之后iframe里的免密登录就没法用了。clipboard-read和clipboard-write:15个站关死。嵌入式组件里的“复制优惠码”按钮就是这么坏掉的,而且坏得完全无声。
两个现网活样本
普查里筛出了两个同时满足三个条件的站:自己锁了关键能力、页面上挂着跨源iframe、而那些iframe上没有委派属性。一个是美妆品牌,头里写着geolocation=(self), camera=(self),首页挂着两个跨源iframe;另一个是订阅计费服务商,四项能力全关,首页挂着一个跨源iframe。按前面那张单变量表,这些iframe里的相关能力全部处于不可用状态,而两个站的服务器都不会知道。
还留着的配置化石
两个站的这行头里只有一项:一个早已废止的兴趣分组指令。那套广告技术方案已经不存在了,指令名浏览器也不再认识——按前面的实测,它会被当成不认识的特性名跳过,属于纯装饰。加固配置是会变成化石的,而且没有任何机制提醒你清理。
该怎么改
把上面所有实测结论压成一份可执行的顺序:
- 先盘点页面上的跨源iframe,再动响应头。不知道自己嵌了什么就写这行头,等于蒙着眼睛开枪。前端那边的协作清单里应该有这一项。
- 不要把默认值原样写进去。这是本文的核心结论:
camera=(self)比不写更严。除非你确实要禁掉委派,否则那一项就别写。 - 要放行第三方组件,在头里显式列出它的源,同时确认iframe上有
allow属性。两把锁缺一不可,源在头里加双引号、在属性里不加。 - 照抄的清单必须逐项对照自己的业务。尤其是支付、通行密钥、剪贴板这三项,它们的故障是无声的。
- 验证必须在两个引擎上各跑一遍。只在一个引擎上试,有一半的概率你测的是“这行头没生效”的那种正常。
- 不要指望上报机制兜底。把关键组件的能力调用结果做成页面内的信标回打,这是目前唯一稳的观测手段,跟后台告警分级那套东西接到一起。
- 变更记录里写清楚这一行影响哪几个组件。不写的话,三个月后没有任何人能把“地图不定位了”和“那次加固”联系起来。这一条应该跟服务器配置清单里其他响应头项放在一起维护。
最后补一句立场。保哥不反对加这行头,反对的是照抄——它确实能挡住一批真实风险,比如被恶意脚本静默调起摄像头。响应头层面的综合治理本来就该做,只是这一行跟别的安全头有个区别:别的头写严了顶多是有人访问不了,这一行写严了是你自己花钱买来的组件不干活,而且账单照付。
常见问题解答
不配置这行响应头,是不是就等于把所有能力都开放了?
正好相反。摄像头、麦克风、定位这些能力的默认名单是self,也就是只有本站自己能用,跨源嵌进来的第三方组件默认就没有。实验台上不发任何策略头、iframe也不写allow属性时,四项能力全部被拒。真正让第三方组件拿到能力的是iframe上的allow属性,那段你从第三方文档里复制过来的嵌入代码,末尾那串allow就是开关。
为什么我把摄像头写成默认值以后,嵌入的组件反而用不了了?
这是实测中最反直觉的一条。头里根本不写摄像头这一项,跨源组件用得了;写上camera=(self)——self正是文档写明的默认值——同一个组件立刻被拒。原因是显式声明之后,父页面的名单里只剩本站一个源,而规范规定禁用是单向开关,子框架的allow属性补不回来。所以除非你确实想禁掉委派,那一项就不要写。
组件被策略挡住的时候,前端代码能不能捕获到?
分能力看。摄像头和麦克风返回被拒的Promise,错误名是NotAllowedError,写了catch就能接住;支付接口在构造时直接抛SecurityError,需要try包起来。定位最麻烦,它走的是错误回调,而那个回调在语法上是可选的,只写成功回调的代码表现为什么都没发生。另外要注意NotAllowedError跟用户手动点拒绝是同一个错误名,光看错误名分不出是谁拒的。
写错了这行头,浏览器会不会报错?
会,但分三档。旧语法、单引号、指令名大写这三类会让整条头解析失败,规范规定此时返回空策略,等于你什么都没写,控制台给一条error级日志;源地址漏了双引号或者夹了不认识的特性名,只影响那一项,控制台给warning;而漏写括号写成camera=self这种,页面正常、控制台一个字没有,只有跨源组件那一格悄悄关上了。全程HTTP状态码都是200。
为什么同一份配置在不同浏览器上表现不一样?
因为有一个主流引擎不执行这行响应头。实测15组里,凡是靠iframe的allow属性判决的,两个引擎结论一致;凡是靠响应头判决的,那个引擎一律放行,包括把四项能力全部写成()的全禁组。MDN在这个头的页面上直接挂了限定可用性的提示,说它在一些使用最广泛的浏览器上不工作。所以验证必须两个引擎各跑一遍。
先上Report-Only观察一段时间,是不是稳妥做法?
意图是对的,但拿不到数据。实验台上Report-Only版本确实只观察不拦截,摄像头照常可用;可四组上报实验一份报文都没收到,而同一个页面、同一个接收端点,用内容安全策略的老式上报机制立刻收到一份格式完整的报文,说明通路本身没问题。权限策略又没有那条老路可退。现网普查里发Report-Only版本的站是0个。稳妥做法是自己在页面里对关键能力做一次调用并把结果回打。
第三方组件从脚本标签换成iframe,会有什么副作用?
会连带收紧它的能力。跨源脚本继承的是宿主页面的策略,跑在你的页面里就享受self这一档;iframe有自己的浏览上下文,被当成外人处理。实测同一行camera=(self)下,脚本方式接入的探针拿得到摄像头,iframe方式接入的同一段代码被拒。改接入方式的理由通常是样式隔离或合规要求,做决定的人一般不会想到能力也跟着变了。
权威参考资料
本文标题:《照着加固清单加的一行响应头,把嵌进来的地图和支付按钮一起变成了摆设》
本文链接:https://zhangwenbao.com/permissions-policy-iframe-third-party-widget-silent-denial.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0