编程笔记

lifelong learning & practice makes perfect

译|在 Go 中防止 CSRF 的现代方法,CrossOriginProtection

Go 1.25 在标准库中引入了一个新的中间件 http.CrossOriginProtection。这让我思考:我们是否终于可以在不依赖基于令牌的检查(如双重提交 Cookie)的情况下防止 CSRF 攻击?是否可以在不引入第三方包(如 justinas/nosurfgorilla/csrf)的情况下构建安全的 Web 应用?

答案是一个谨慎的 “是” —— 只要满足一些重要条件。


http.CrossOriginProtection 中间件

该中间件通过检查请求中的 Sec-Fetch-SiteOrigin 头来判断请求来源。它会自动拒绝来自非同源的 非安全请求(如 POST、PUT),并返回 403 Forbidden

工作原理

  • 现代浏览器 会自动在请求中包含 Sec-Fetch-Site 头。
  • 如果请求来源与目标页面同源,则头部为 same-origin
  • 如果不同源,则 http.CrossOriginProtection 会拒绝请求。
  • 如果没有 Sec-Fetch-Site,则回退检查 OriginHost 是否匹配。
  • 如果两者都不存在,则认为请求不是来自浏览器,允许通过。
  • 仅对 非安全方法(POST、PUT 等)进行检查,GET/OPTIONS 等安全方法始终允许。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"log/slog"
"net/http"
"os"
)

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", home)

slog.Info("starting server on :4000")
err := http.ListenAndServe(":4000", http.NewCrossOriginProtection(mux))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}

func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello!")
}

如果您愿意,也可以配置 http.CrossOriginProtection 的行为。配置选项包括能够添加受信任的来源(允许来自这些来源的跨域请求),以及能够为被拒绝的请求使用自定义处理程序,而不是默认的 403 Forbidden 响应。

想自定义行为时,可以使用如下模式:

文件: main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"log/slog"
"net/http"
"os"
)

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", home)

slog.Info("starting server on :4000")

err := http.ListenAndServe(":4000", preventCSRF(mux))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}

func preventCSRF(next http.Handler) http.Handler {
cop := http.NewCrossOriginProtection()

cop.AddTrustedOrigin("https://foo.example.com")

cop.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("CSRF check failed"))
}))

return cop.Handler(next)
}

func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello!")
}

限制

http.CrossOriginProtection 的主要限制是它只对阻止来自现代浏览器的请求有效。您的应用程序仍然容易受到来自不包含 Sec-Fetch-SiteOrigin 头(通常是 2020 年之前的)的旧版浏览器的 CSRF 攻击。

目前,浏览器对 Sec-Fetch-Site 头的支持率为 92%,对 Origin 头为 95%。因此,通常情况下,仅仅依靠 http.CrossOriginProtection 不足以作为您唯一的 CSRF 防护措施。

还需要注意的是,只有当您的应用程序具有“可信来源”时才会发送 Sec-Fetch-Site 头——这基本上意味着您的应用程序在生产环境中使用 HTTPS(或开发期间使用 localhost),http.CrossOriginProtection 才能充分发挥作用。

您还应该知道,当请求中不存在 Sec-Fetch-Site 头,并且回退到比较 OriginHost 头时,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-SiteOrigin 头之一呢?

据我从 MDN 兼容性数据和 Can I Use 网站的表格来看,答案是“是”,适用于(几乎)所有主流浏览器。

如果您强制使用 TLS 1.3 作为最低版本:

  • 不支持 TLS 1.3 的旧版浏览器根本无法连接到您的应用程序。
  • 对于支持 TLS 1.3 并可以连接的现代主流浏览器,您可以确信至少支持 Sec-Fetch-SiteOrigin 头之一——因此 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.comhttps://www.example.com 不是相同的源,因为主机名 (example.comwww.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.comhttps://www.example.comhttps://login.admin.example.com 都被认为是同站的,因为方案 (https) 和可注册域 (example.com) 相同。它们之间的请求不会被认为是跨站的,但会是跨源的。

注意:某些浏览器版本使用不同的同站定义,它不要求相同的方案,只要求相同的可注册域。对于这些浏览器版本,https://admin.example.comhttp://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 属性自 2017 年起普遍受到网络浏览器的支持,Go 自 v1.11 起也支持。如果您在 Cookie 上设置 SameSite=LaxSameSite=Strict 属性,则该 Cookie 将仅包含在发送到设置它的同一站点的请求中。反过来,这可以防止跨站请求伪造攻击(但不能防止来自同一站点内的跨源攻击)。

这里有一些好消息——所有支持 TLS 1.3 的主流浏览器也都完全支持 SameSite Cookie,据我所知没有例外。因此,如果您强制使用 TLS 1.3,您可以确信所有使用您应用程序的主流浏览器都会遵守 SameSite 属性。

这意味着通过在 Cookie 上使用 SameSite=LaxSameSite=Strict,您可以消除我们之前谈到的 Firefox v60-69 引起的跨站请求伪造风险。

综合考量

如果您结合使用 HTTPS,强制将 TLS 1.3 作为最低版本,适当使用 SameSite=LaxSameSite=Strict Cookie,并在您的应用程序中使用 http.CrossOriginProtection 中间件,据我所知,主流浏览器只剩下两个未被缓解的 CSRF/CORF 风险:

  1. Firefox v60-69 中来自同一站点的 CORF 攻击(即来自您的可注册域下的另一个子域)。
  2. 从您的源的 HTTP 版本发起的 CORF 攻击,来自不支持 Sec-Fetch-Site 头的浏览器。

对于第一个风险,如果您在可注册域下没有任何其他网站,或者您确信这些网站是安全的且未被攻破,那么鉴于 Firefox v60-69 的使用率极低,这可能是一个您愿意接受的风险。

对于第二个风险,如果您的源根本不支持 HTTP(包括重定向),那么您无需担心这一点。否则,您可以通过在 HTTPS 响应中包含 HSTS 头来缓解风险。

在本文的开头,我提到在某些条件下不使用基于令牌的 CSRF 检查可能是可以的。那么,让我们回顾一下这些条件是什么:

  • 您的应用程序使用 HTTPS 并强制将 TLS 1.3 作为最低版本。您接受使用旧版浏览器的用户将根本无法连接到您的应用程序。
  • 您遵循良好实践,绝不响应使用安全方法 GET、HEAD、OPTIONS 或 TRACE 的请求来更改重要的应用程序状态。
  • 您同时使用 http.CrossOriginProtection 中间件和 SameSite=LaxSameSite=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 攻击的影响既孤立又轻微。

原文

欢迎关注我的其它发布渠道