Go 1.25 在标准库中引入了一个新的中间件 http.CrossOriginProtection
。这让我思考:我们是否终于可以在不依赖基于令牌的检查(如双重提交 Cookie)的情况下防止 CSRF 攻击?是否可以在不引入第三方包(如 justinas/nosurf
或 gorilla/csrf
)的情况下构建安全的 Web 应用?
答案是一个谨慎的 “是” —— 只要满足一些重要条件。
http.CrossOriginProtection
中间件
该中间件通过检查请求中的 Sec-Fetch-Site
和 Origin
头来判断请求来源。它会自动拒绝来自非同源的 非安全请求(如 POST、PUT),并返回 403 Forbidden
。
工作原理
- 现代浏览器 会自动在请求中包含
Sec-Fetch-Site
头。 - 如果请求来源与目标页面同源,则头部为
same-origin
。 - 如果不同源,则
http.CrossOriginProtection
会拒绝请求。 - 如果没有
Sec-Fetch-Site
,则回退检查Origin
与Host
是否匹配。 - 如果两者都不存在,则认为请求不是来自浏览器,允许通过。
- 仅对 非安全方法(POST、PUT 等)进行检查,GET/OPTIONS 等安全方法始终允许。
使用示例
1 | package main |
如果您愿意,也可以配置 http.CrossOriginProtection
的行为。配置选项包括能够添加受信任的来源(允许来自这些来源的跨域请求),以及能够为被拒绝的请求使用自定义处理程序,而不是默认的 403 Forbidden 响应。
想自定义行为时,可以使用如下模式:
文件: main.go
1 | package main |
限制
http.CrossOriginProtection
的主要限制是它只对阻止来自现代浏览器的请求有效。您的应用程序仍然容易受到来自不包含 Sec-Fetch-Site
或 Origin
头(通常是 2020 年之前的)的旧版浏览器的 CSRF 攻击。
目前,浏览器对 Sec-Fetch-Site
头的支持率为 92%,对 Origin
头为 95%。因此,通常情况下,仅仅依靠 http.CrossOriginProtection
不足以作为您唯一的 CSRF 防护措施。
还需要注意的是,只有当您的应用程序具有“可信来源”时才会发送 Sec-Fetch-Site
头——这基本上意味着您的应用程序在生产环境中使用 HTTPS(或开发期间使用 localhost),http.CrossOriginProtection
才能充分发挥作用。
您还应该知道,当请求中不存在 Sec-Fetch-Site
头,并且回退到比较 Origin
和 Host
头时,Host
头不包含方案。这个限制意味着当不存在 Sec-Fetch-Site
头但存在 Origin
头时,http.CrossOriginProtection
会错误地允许从 http://{host}
到 https://{host}
的跨域请求。为了减轻这种风险,您理想情况下应该配置您的应用程序使用 HTTP 严格传输安全 (HSTS)。
强制使用 TLS 1.3
深入研究这个问题让我开始思考… 如果您已经计划使用 HTTPS 并强制使用 TLS 1.3 作为最低支持的 TLS 版本呢?您能否确信所有支持 TLS 1.3 的网络浏览器也支持 Sec-Fetch-Site
或 Origin
头之一呢?
据我从 MDN 兼容性数据和 Can I Use 网站的表格来看,答案是“是”,适用于(几乎)所有主流浏览器。
如果您强制使用 TLS 1.3 作为最低版本:
- 不支持 TLS 1.3 的旧版浏览器根本无法连接到您的应用程序。
- 对于支持 TLS 1.3 并可以连接的现代主流浏览器,您可以确信至少支持
Sec-Fetch-Site
或Origin
头之一——因此http.CrossOriginProtection
将有效工作。
我能看到的唯一例外是 Firefox v60-69 (2018-2019),它不支持 Sec-Fetch-Site
头,并且不为 POST 请求发送 Origin
头。这意味着 http.CrossOriginProtection
将无法有效阻止来自该浏览器的请求。Can I Use 显示 Firefox v60-69 的使用率为 0%,因此这里的风险似乎非常低——但世界上某个地方可能仍然有一些计算机在运行它。
此外,我们只掌握了主流浏览器(Chrome/Chromium、Firefox、Edge、Safari、Opera 和 Internet Explorer)的信息。但当然,还存在其他浏览器。它们大多数是 Chromium 或 Firefox 的分支,因此很可能没问题,但这里没有保证,并且很难量化风险。
因此,如果您使用 HTTPS 并强制使用 TLS 1.3,这是确保 http.CrossOriginProtection
有效工作的一大步。然而,Firefox v60-69 和非主流浏览器仍然存在非零风险,因此您可能希望增加一些纵深防御并同时使用 SameSite Cookie。
我们稍后会更多地讨论 SameSite Cookie,但首先我们需要快速绕道讨论“源 (origin)”和“站点 (site)”这两个术语之间的区别。
跨站点与跨源
在 Web 规范和 Web 浏览器的世界中,跨站点 (cross-site) 和跨源 (cross-origin) 是细微不同的概念,在这样的安全上下文中,理解它们之间的区别并确切地表达我们的意思非常重要。
我将快速解释。
如果两个网站共享完全相同的方案 (scheme)、主机名 (hostname) 和端口(如果存在),则它们具有相同的源 (origin)。因此 https://example.com
和 https://www.example.com
不是相同的源,因为主机名 (example.com
和 www.example.com
) 不同。它们之间的请求将是跨源的。
如果两个网站共享相同的方案和可注册域 (registerable domain),则它们是“同站 (same site)”的。
注意:可注册域是主机名中紧邻(并包括)有效顶级域 (effective TLD) 的部分。以下是一些示例:
- 对于
https://www.google.com/
,顶级域是com
,可注册域是google.com
。 - 对于
https://login.mail.ucla.edu
,顶级域是edu
,可注册域是ucla.edu
。 - 对于
https://www.gov.uk
,顶级域是gov.uk
,可注册域是www.gov.uk
。
您可以在此处找到有效顶级域的完整列表。
因此,https://example.com
、https://www.example.com
和 https://login.admin.example.com
都被认为是同站的,因为方案 (https
) 和可注册域 (example.com
) 相同。它们之间的请求不会被认为是跨站的,但会是跨源的。
注意:某些浏览器版本使用不同的同站定义,它不要求相同的方案,只要求相同的可注册域。对于这些浏览器版本,https://admin.example.com
和 http://blog.example.com
也将被视为同站。
如今,这通常被称为无方案同站 (schemaless same-site),但在历史版本或文档中,它可能只被称为同站。
那么,我在这里想要表达的要点是什么呢?
Go 的 http.CrossOriginProtection
中间件的命名是准确且恰当的。它阻止跨源请求。它比只阻止跨站请求更严格,因为它也阻止来自同一站点(即可注册域下的其他源)的请求。
这很有用,因为它有助于防止您的老旧、十多年未更新的 WordPress 博客 https://blog.example.com
被攻破,并被用来向您的重要网站 https://admin.example.com
发起请求伪造攻击的情况。
当大多数人——包括我自己在内——随意谈论“CSRF 攻击”时,我们大多数时候指的是实际上是跨源请求伪造 (cross-origin request forgery),而不仅仅是跨站请求伪造 (cross-site request forgery)。很遗憾 CSRF 是描述这类攻击的常用和已知缩写,因为大多数时候 CORF 会更准确和恰当。但嘿!这就是我们所处的混乱世界。
然而,在本文的其余部分,当我的意思确实是跨源请求伪造时,我将使用 CORF 代替 CSRF。
SameSite Cookie
SameSite Cookie 属性自 2017 年起普遍受到网络浏览器的支持,Go 自 v1.11 起也支持。如果您在 Cookie 上设置 SameSite=Lax
或 SameSite=Strict
属性,则该 Cookie 将仅包含在发送到设置它的同一站点的请求中。反过来,这可以防止跨站请求伪造攻击(但不能防止来自同一站点内的跨源攻击)。
这里有一些好消息——所有支持 TLS 1.3 的主流浏览器也都完全支持 SameSite Cookie,据我所知没有例外。因此,如果您强制使用 TLS 1.3,您可以确信所有使用您应用程序的主流浏览器都会遵守 SameSite 属性。
这意味着通过在 Cookie 上使用 SameSite=Lax
或 SameSite=Strict
,您可以消除我们之前谈到的 Firefox v60-69 引起的跨站请求伪造风险。
综合考量
如果您结合使用 HTTPS,强制将 TLS 1.3 作为最低版本,适当使用 SameSite=Lax
或 SameSite=Strict
Cookie,并在您的应用程序中使用 http.CrossOriginProtection
中间件,据我所知,主流浏览器只剩下两个未被缓解的 CSRF/CORF 风险:
- Firefox v60-69 中来自同一站点的 CORF 攻击(即来自您的可注册域下的另一个子域)。
- 从您的源的 HTTP 版本发起的 CORF 攻击,来自不支持
Sec-Fetch-Site
头的浏览器。
对于第一个风险,如果您在可注册域下没有任何其他网站,或者您确信这些网站是安全的且未被攻破,那么鉴于 Firefox v60-69 的使用率极低,这可能是一个您愿意接受的风险。
对于第二个风险,如果您的源根本不支持 HTTP(包括重定向),那么您无需担心这一点。否则,您可以通过在 HTTPS 响应中包含 HSTS 头来缓解风险。
在本文的开头,我提到在某些条件下不使用基于令牌的 CSRF 检查可能是可以的。那么,让我们回顾一下这些条件是什么:
- 您的应用程序使用 HTTPS 并强制将 TLS 1.3 作为最低版本。您接受使用旧版浏览器的用户将根本无法连接到您的应用程序。
- 您遵循良好实践,绝不响应使用安全方法 GET、HEAD、OPTIONS 或 TRACE 的请求来更改重要的应用程序状态。
- 您同时使用
http.CrossOriginProtection
中间件和SameSite=Lax
或SameSite=Strict
Cookie。使用 SameSite Cookie 对于一般的纵深防御很重要,更具体地说,是为了缓解来自 Firefox v60-69 的 CSRF 攻击。 - 由于来自 Firefox v60-69 的未受保护的同站 CORF 攻击风险,您要么在您的可注册域下没有任何其他网站,要么您确信它们是安全且未被攻破的。
- 您的应用程序源根本没有 HTTP 版本,或者您在 HTTPS 响应中包含 HSTS 头。
- 最后,您愿意接受来自非主流浏览器(支持 TLS 1.3 但不支持 Origin 或 Sec-Fetch-Site 头或 SameSite Cookie)的 CSRF/CORF 攻击的难以量化的风险。是否存在这样的浏览器?我不知道,我也不确定是否有办法 100% 确定地回答这个问题。因此,您需要在此处进行自己的风险评估,这可能是一个您只愿意接受的风险,如果您的应用程序是一个低价值目标,并且成功进行 CSRF/CORF 攻击的影响既孤立又轻微。