Cross-site request forgery (CSRF),跨站请求伪造,是 web 安全中最常见的类型之一。网站往往会用 cookie 来标识某一浏览器的用户已经登录,CSRF 利用这种机制来做攻击。
攻击
常规例子
设想一个银行提供了这样的 GET 接口用来转帐:
https://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory
假如你刚好登录了这个银行,银行下发了 cookie 表示你已在登录状态。你再访问了一个恶意网站,网站上放了一张图片:
<img src="https://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory">
此时你访问这个网站时,你就无意中执行了转帐接口。
即使这个接口需要 POST 方式,恶意网站仍可通过 JS 来达到目的,比如在隐藏的 iframe 中放入这些代码:
<form action="https://bank.example.com/withdraw" method="POST">
<input type="hidden" name="account" value="bob">
<input type="hidden" name="amount" value="1000000">
<input type="hidden" name="for" value="mallory">
</form>
<script>window.addEventListener('DOMContentLoaded', (e) => { document.querySelector('form').submit(); }</script>
Login CSRF
参考自 OWASP 的 CSRF cheatsheet。
有些开发者会认为登陆的接口不需要做 CSRF 保护,因为此时用户未登陆,没有被偷身份的风险。但事实上仍有别的风险。比如正常用户访问恶意网站时,网站所有者通过 CSRF 将其自己的身份登陆到电商网站;正常用户再访问电商网站时,可能不知不觉把自己的信用卡信息增加到了攻击者的帐号上。因此同样的 CSRF 防范手段应该被使用上。
防范
防止 CSRF 有几个方法。
服务器检查 HTTP 头中的 Origin 及 Referer 字段值
这两个字段会告诉服务器,发起请求时用户当前的页面信息。如果你在恶意网站中被发起请求,那么服务器知道你并不是处在正常的网站中,可以拦截掉这些请求。这是最简单的检查手段。
Anti-CSRF tokens
即当你访问正常的网站页面时,服务器在 Form 表单中会给你返回一个 csrf_token
:
<form action="https://report-uri.io/login/auth" method="POST">
<input type="hidden" name="csrf_token" value="d82c90fc4a14b01224gde6ddebc23bf0">
<input type="email" id="email" name="email">
<input type="password" id="password" name="password">
<button type="submit" class="btn btn-primary">Login</button>
</form>
当你提交表单时,这个 token 也会被带上来,服务器在服务端再检查 token 是否匹配。
对于恶意网站,它受限于 CORS 规则无法加载正常的页面,因此它也拿不到相应 token,所以无法构造带有 token 的恶意请求。
这种方法依赖于正常网站的服务端功能。大部分 web 框架已内置此功能。
通过 cookie 的 SameSite 属性控制
相较于上面两种方法,新推出的 SameSite 则从根本的 cookie 访问机制中解决这个问题。看 HTTP: Cookie: SameSite。
实践中,anti-CSRF token 和 SameSite 两种机制应该同时被应用。SameSite 作为一个较新的指令,在一些老的浏览器中并不被支持。