1. 简介
有趣的网页应用通常会有大量对外暴露的端点,这些端点可能暴露用户的敏感数据,或以用户身份执行某些操作。由于用户的浏览器很容易被诱导去访问这些端点,并在请求中带上用户现有的凭据(如 cookies、内网特权身份等),因此应用程序在设计这些端点时必须非常谨慎,以防止滥用。
在某些情况下做到谨慎非常困难(如“简单”的CSRF),而在另一些情况(如跨站搜索、计时攻击等)几乎不可能。后一类问题包括基于服务器生成特定响应时的处理时长的计时攻击,以及基于响应长度的各种攻击(包括面向Web的计时攻击和被动网络攻击者)。
如果服务器能够根据请求的方式更智能地决定是否响应,将有助于缓解后一类问题。例如,一个银行服务器上的“转出我所有资金”的端点几乎不可能被放在
img
标签中引用,同样,evil.com 发起任何合法请求的概率也非常低。理想情况下,服务器可以事先拒绝这些请求,而不是将其送到应用后端处理。
在这里,我们介绍了一种机制,用户代理可通过为外发请求添加更多上下文,实现上述决策。通过在一组Fetch 元数据请求头中向服务器传递元数据,应用可以基于若干前置条件快速拒绝请求。这项工作甚至可以提升到应用层之上(如反向代理、CDN等)来完成。
1.1. 示例
一个由
picture
元素生成的请求,会包含如下HTTP请求头:
Sec-Fetch-Dest: image Sec-Fetch-Mode: no-cors Sec-Fetch-Site: cross-site
如果用户点击页面内的链接,从 https://example.com 跳转到 https://example.com/,则会生成包含如下HTTP请求头的请求:
Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1
2. Fetch 元数据请求头
下文定义了多个Fetch 元数据请求头,每个都将某个有意义的请求属性暴露给服务器。
2.1. Sec-Fetch-Dest HTTP请求头
Sec-Fetch-Dest HTTP 请求头会将请求的目标类型暴露给服务器。它是一个结构化字段,其值必须为标记(token)。[RFC9651]
其ABNF如下:
Sec-Fetch-Dest = sf-token
Sec-Fetch-Dest 的有效值包括 请求在 Fetch标准中定义的所有目标类型。
为了支持尚未定义的未来请求类型,若遇到无效值,服务器应忽略此头。
//fetch()的 destination 为空字符串: Sec-Fetch-Dest: empty //<img>的 destination 为 "image" Sec-Fetch-Dest: image //new Worker()的 destination 为 "worker" Sec-Fetch-Dest: worker // 顶层导航的 destination 为 "document" Sec-Fetch-Dest: document //<iframe>导航的 destination 为 "iframe" Sec-Fetch-Dest: iframe
Sec-Fetch-Dest 头:
-
断言:r 的url为可能值得信任的 URL。
-
如果 r 的目标类型为空字符串,则将 header 的值设为字符串 "
empty"。否则,将 header 的值设为 r 的目标类型。注:将 Fetch 的空字符串 destination 显式映射为 "
empty" token,是为了简化处理。
2.2. Sec-Fetch-Mode HTTP请求头
Sec-Fetch-Mode HTTP请求头会将请求的模式(mode)暴露给服务器。它是一个结构化字段,其值必须为标记(token)。[RFC9651]
其ABNF如下:
Sec-Fetch-Mode = sf-token
Sec-Fetch-Mode 的有效值包括 "cors"、"navigate"、
"no-cors"、"same-origin" 和 "websocket"。为支持尚未定义的新类型请求,若遇到无效值,服务器应忽略此头。
Sec-Fetch-Mode 头:
2.3. Sec-Fetch-Site HTTP请求头
Sec-Fetch-Site HTTP请求头会暴露请求的发起方与目标的来源关系。它是一个结构化字段,值为标记(token)。[RFC9651]
其ABNF如下:
Sec-Fetch-Site = sf-token
Sec-Fetch-Site 的有效值包括 "cross-site"、"same-origin"、
"same-site" 和 "none"。
为支持未知的未来请求类型,遇到无效值时服务器应忽略此头。
Sec-Fetch-Site 头:
-
断言:r 的url为可能值得信任的 URL。
-
将 header 的值设为
same-origin。 -
如果 r 是一个导航请求,且明确由用户操作触发(例如,直接在用户代理输入地址,或点击书签等),则将 header 的值设为
none。注:更详细解释见§ 4.3 用户直接发起的请求。
-
如果 header 的值不是
none,则对 r 的url 列表中的每个 url:
2.4. Sec-Fetch-User HTTP请求头
Sec-Fetch-User HTTP请求头用于标识导航请求是否由用户激活触发。它是一个结构化字段,值为布尔值。[RFC9651]
其ABNF如下:
Sec-Fetch-User = sf-boolean
注:该头仅用于导航请求,且仅在其值为 true
时传递。未来若能明确哪些场景受益,并定义所有相关子资源请求的用户激活状态,也许可以扩展到更广泛场景,但目前导航请求已经有清晰的用例并易于一致性实现。
Sec-Fetch-User 头:
-
断言:r 的url为可能值得信任的 URL。
-
如果 r 不是导航请求,或 r 的 user-activation 为
false,则返回。 -
将 header 的值设为
true。
3. 与 Fetch 和 HTML 的集成
为了支持 Sec-Fetch-User,request 拥有 user-activation,默认为 false,
除非由 HTML 的 create navigation params by fetching 算法赋值。
Fetch 元数据请求头在 Fetch 的 "HTTP-network-or-cache" 算法中被附加到外发请求,具体步骤如下。集成细节请参阅规范 [FETCH]。
-
如果 r 的 url 不是 可能值得信任的 URL,则返回。
4. 安全与隐私注意事项
4.1. 重定向
用户代理会在重定向链中的每个请求上都携带 Sec-Fetch-Site
头。当出现跨源或跨站点重定向时,该头的值会随之变化以减少混淆。
用于设置
Sec-Fetch-Site 头的算法遍历 request 的全部 url 列表,如果列表中有任何 URL 与请求的 当前 url 跨站,则发送 cross-site;仅当所有 URL 均为同站时,值才为
same-site;仅当所有 URL 均为同源时,值才为 same-origin。
例如,如果 https://example.com/ 请求 https://example.com/redirect,初始请求的
Sec-Fetch-Site 值为 same-origin。如果该响应重定向到
https://subdomain.example.com/redirect,新请求的 Sec-Fetch-Site 值为
same-site(因为二者同属于可注册域)。如果再次重定向到 https://example.net/redirect,新的
Sec-Fetch-Site 值为 cross-site(因其与前两个 URL 非同站)。如果后续重定向又回到
https://example.com/,由于链路中已经出现非同站 URL,最终请求的 Sec-Fetch-Site 依然是
cross-site。
注:对于 Sec-Fetch-Site: None
的特殊情况,通常应在整个重定向过程中保持该值,用于支持用户在地址栏中粘贴短链接的场景。例如,如果用户通过地址栏导航到 https://sho.rt/link 时为
Sec-Fetch-Site: none,那么重定向到 https://target.com/long/path/goes/here 后也应继续为
Sec-Fetch-Site: none。
4.2. Sec- 前缀
本文档定义的所有头字段都以 Sec- 为前缀,因此它们都是 禁止的响应头名称,无法被 JavaScript
修改。这能防止恶意网站诱使用户代理发送伪造的元数据,有助于网站基于这些信息做出安全决策。
4.3. 用户直接发起的请求
在设置
Sec-Fetch-Site 头时,用户代理需区分“明确由用户交互引发的导航请求”。这一说法参考自 HTML 规范,提出:“除规范明确定义之外,用户代理可为用户提供多种明确发起浏览上下文导航的方式。”
目标是区分受站点(可能为恶意)的“网页导航”(如链接跳转、window.location 赋值、表单提交等)和真正由用户意图驱动的操作(如地址栏输入、书签点击等)。前者的
Sec-Fetch-Site 头会被设置为 same-origin、same-site 或
cross-site。后者的值则为 none,表示并无特定站点引发该请求,因此可被服务器视作可信用户行为。
不同用户代理在交互细节上可能存在实现差异,这些场景很难实现完全自动化测试。不过主流行为还是可以协调一致,以下是常见示例:
-
从地址栏导航:通常这种导航应被视为用户直接操作,包括
Sec-Fetch-Site: none。用户粘贴内容到地址栏(尤其复制源可关联到特定 origin),用户代理也可区别处理。 -
通过用户代理 UI 导航(比如书签、新标签页):与地址栏输入类似,应视为用户直接操作,头字段值为
Sec-Fetch-Site: none。 -
通过链接右键菜单导航(如“在新窗口打开”):由于目标页由原页面控制,该场景应仍按网页控制处理,
Sec-Fetch-Site应基于当前页面和目标页面关系赋值。 -
Ctrl-点击链接:同上,与右键菜单跳转逻辑一致。
-
历史导航(如浏览器后退按钮):
-
拖拽:可以根据被拖拽内容的来源区分。如果来源于某标签页,则可确定其 origin 并据此设置
Sec-Fetch-Site。若来源于书签栏、其它应用等,则建议为Sec-Fetch-Site: none。
4.4. 扩展发起的请求
部分用户代理支持扩展插件发起请求,这类扩展往往拥有远超普通网页内容的能力,为用户提供更多控制 Web 的方式。虽然这一部分超出了 Web 平台的直接规范范围,但用户代理仍应仔细考虑这些请求应如何向服务器展示。总体而言,应遵循两个目标:
-
对于没有某站点特殊权限的扩展,不能允许其发起绕过服务端 Fetch 元数据校验的请求。
-
开发者可以识别扩展发起的请求,从而在必要时将其从服务端的 Fetch 元数据校验中豁免。这有利于在不影响用户正常需求的前提下安全部署该机制。
基于上述考虑,建议用户代理实现以下行为:
-
如果扩展对某 URL 无访问权限,则其请求的
Sec-Fetch-Site应为cross-site,与普通网页请求一致;若拥有该权限,则可为same-origin。 -
扩展上下文的请求可附带
Origin头,值为实现自定义的特殊字符串,从而帮助服务器区分扩展发起的请求与网页发起的请求。
5. 部署注意事项
5.1. Vary
如果某端点的响应依赖于客户端提供的 Fetch 元数据头,开发者应当谨慎地在响应中包含合适的 Vary 头 [RFC9110],确保缓存系统合理处理该响应。例如:
Vary: Accept-Encoding, Sec-Fetch-Site。
5.2. 头部冗余
本规范早期版本曾定义过单一 Sec-Metadata 字典型头字段。经多方讨论(包括 Mark Nottingham 的 [mnot-designing-headers]),设计转向为一组简单头字段,每个仅承载单一
token。新设计在现有 HTTP HPACK 压缩下表现更优。
更多讨论详见 w3ctag/design-reviews#280。
6. IANA 注意事项
应在永久报文头字段注册表中为下述 Fetch 元数据头进行登记: [RFC3864]
6.1. Sec-Fetch-Dest 登记
- 头字段名称
-
Sec-Fetch-Dest
- 适用协议
-
http
- 状态
-
standard
- 作者/变更控制者
-
Me
- 规范文档
-
本规范(参见 § 2.1 Sec-Fetch-Dest HTTP 请求头)
6.2. Sec-Fetch-Mode 登记
- 头字段名称
-
Sec-Fetch-Mode
- 适用协议
-
http
- 状态
-
standard
- 作者/变更控制者
-
Me
- 规范文档
-
本规范(参见 § 2.2 Sec-Fetch-Mode HTTP 请求头)
6.3. Sec-Fetch-Site 登记
- 头字段名称
-
Sec-Fetch-Site
- 适用协议
-
http
- 状态
-
standard
- 作者/变更控制者
-
Me
- 规范文档
-
本规范(参见 § 2.3 Sec-Fetch-Site HTTP 请求头)
6.4. Sec-Fetch-User 登记
- 头字段名称
-
Sec-Fetch-User
- 适用协议
-
http
- 状态
-
standard
- 作者/变更控制者
-
Me
- 规范文档
-
本规范(参见 § 2.4 Sec-Fetch-User HTTP 请求头)
7. 致谢
感谢 Anne van Kesteren、Artur Janc、Dan Veditz、Łukasz Anforowicz、Mark Nottingham 和 Roberto Clapis,他们为该机制设计提供了大量支持。