The Copenhagen Book

跨站请求伪造 (CSRF)

概述

CSRF攻击允许攻击者在用户的凭证存储在Cookie中时,代表用户发起已认证的请求。

当客户端发起跨域请求时,浏览器会发送预检请求以检查请求是否被允许(CORS)。但对于一些“简单”请求,例如表单提交,这一步被省略。由于即使对于跨域请求也自动包含Cookie,这使恶意攻击者能在不直接窃取任何域的令牌的情况下,作为认证用户发起请求。同源策略默认禁止跨域客户端读取响应,但请求仍会通过。

例如,如果您登录了bank.com,即使表单托管在不同的域上,您的会话Cookie仍会随着此表单提交发送。

<form action="https://bank.com/send-money" method="post">
  <input name="recipient" value="attacker" />
  <input name="value" value="$100" />
  <button>Send money</button>
</form>

这也可以通过fetch()请求实现,因此不需要用户输入。

const body = new URLSearchParams();
body.set("recipient", "attacker");
body.set("value", "$100");

await fetch("https://bank.com/send-money", {
  method: "POST",
  body
});

跨站与跨域

当请求在两个完全不同的域之间时,被认为是跨站和跨域请求,而在两个子域之间则仅被视为跨域请求。虽然跨站请求伪造(CSRF)暗示跨站请求,但默认应严格对待,也要防范跨域攻击。

防护措施

可以通过仅接受来自可信来源的浏览器发起的POST及类似POST的请求来防止CSRF。

所有处理表单的路由都必须实施保护措施。如果您的应用程序当前不使用表单,也至少应检查Origin头以防止未来的问题。通常建议只使用POST及类似的请求方法(如PUT, DELETE等)来修改资源。

对于常见的基于令牌的方法,令牌不应为单次使用的(例如每次表单提交新的令牌),因为这会在按下返回按钮时导致问题。同时,页面应有严格的跨域资源共享(CORS)策略。如果Access-Control-Allow-Credentials不严格,恶意站点可以发送GET请求获取具有有效CSRF令牌的HTML表单。

反CSRF令牌

这是一个非常简单的方法,每个会话都有一个唯一的CSRF 令牌 关联。

<form method="post">
  <input name="message" />
  <input type="hidden" name="__csrf" value="<CSRF_TOKEN>" />
  <button>Submit</button>
</form>

如果无法在服务器上存储令牌,可以使用签名双提交Cookie。这不同于基本的双提交Cookie,因为表单中包含的令牌使用密钥签名。

使用HMAC SHA-256和密钥生成新的令牌并对其进行哈希处理。

func generateCSRFToken() (string, []byte) {
	buffer := [10]byte{}
	crypto.rand.Read(buffer)
	csrfToken := base64.StdEncoding.encodeToString(buffer)
	mac := hmac.New(sha256.New, secret)
	mac.Write([]byte(csrfToken))
	csrfTokenHMAC := mac.Sum(nil)
	return csrfToken, csrfTokenHMAC
}

// 可选择将Cookie与特定会话ID关联。
func generateCSRFToken(sessionId string) (string, []byte) {
	// ...
	mac.Write([]byte(csrfToken + "." + sessionId))
	csrfTokenHMAC := mac.Sum(nil)
	return csrfToken, csrfTokenHMAC
}

令牌存储为Cookie,HMAC存储在表单中。Cookie应具有SecureHttpOnlySameSite标志。要验证请求,可以使用Cookie验证表单数据中发送的签名。

常规双提交Cookie如果没有签名,在攻击者访问应用程序域的子域时仍可能导致漏洞。这将允许他们设置自己的双提交Cookie。

Origin头

防止CSRF攻击的一个简单方法是检查非GET请求的Origin头。这是一个新引入的头,包含请求的来源。如果依赖该头,重要的是应用程序不使用GET请求来修改资源。

虽然Origin头可以通过自定义客户端伪造,但关键是不能通过客户端JavaScript伪造。用户仅在使用浏览器时容易受到CSRF攻击。

func handleRequest(w http.ResponseWriter, request *http.Request) {
    if request.Method != "GET" {
        originHeader := request.Header.Get("Origin")
        // 还可以将其与Host或X-Forwarded-Host头进行比较。
        if originHeader != "https://example.com" {
            // 请求来源无效
            w.WriteHeader(403)
            return
        }
    }
    // ...
}

大约自2020年以来,所有现代浏览器都支持Origin头,尽管Chrome和Safari早在之前就支持它。如果未包含Origin头,不允许请求。

Referer头是Origin头之前引入的类似头。当Origin头未定义时可作为回退。

会话Cookie应具有SameSite标志。此标志决定浏览器在何时在请求中包含Cookie。SameSite=Lax的Cookie仅在使用安全HTTP方法(如GET)的跨站请求中发送,而SameSite=Strict的Cookie不会在任何跨站请求中发送。建议默认使用Lax,因为Strict的Cookie不会在用户通过外部链接访问您网站时发送。

如果设置为Lax,应用程序不应使用GET请求来修改资源。SameSite标志的浏览器支持显示它目前对96%的网络用户可用。需要注意的是,该标志仅保护跨站请求伪造(不保护跨域请求伪造),一般不应作为唯一的防御措施。