首先介绍一下CSRF的验证过程:
client发送get请求 -> 服务生成XSRF-TOKEN,存入用户session中。返回响应,响应包含XSRF-TOKEN cookie -> 浏览器接收响应后自动保存cookie -> js获取此cookie的值,构造X-XSRF-TOKEN头部 -> 向server发送post请求 -> server通过client传递过来的包含server_id的cookie反序列化此用户的session数据 -> 从session中拿出用户的CSRF-TOKEN与请求头中的值进行对比 -> 验证通过或失败
然后想象这么一种场景。有一个正常域名A被XSS注入,其中的恶意代码如下:
<img src="http://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory">
这时,当你打开此页面后,浏览器会默认发起http请求,请求地址就是src
中的地址。因为是浏览器发出的请求,所以这个请求默认会带上此域名的cookies。如果恰好你在bank.example.com
这个域名是登陆状态,那么就会中招了。
这里重点强调一下cookie的机制:浏览器向一个域名发起http请求时会带上浏览器保存的关于那个域名的cookies,而不管你从哪个网站发请求。上面的例子,请求就是从域名A发出的。
清楚了上面的攻击手段,对于CSRF验证令牌为什么放在头部这个问题就很好理解了。为了添加新的头部,js就必须获取XSRF-TOKEN
的值,然后构造X-XSRF-TOKEN
头部。而对于上面那种攻击手段,是没有能力获取cookie的。
那么问题又来了,既然域名A都已经被入侵了,那攻击者完全可以植入js代码,然后获取cookie,构造头部,这一系列操作不也是轻而易举的吗?
答案是,不行。因为js要获取cookie存在三个因素的限制:
- Path。也就是URI的路径,路径不同,不允许访问
- Doamin。域名不同不允许访问
- http-only。设置为true之后,js不能访问
很显然,因为当前发起请求所在的域名和攻击者的目标域名不一样,所以即使页面被注入的js代码,也无法获取目标域名的cookie。其次,对于存放session_id
的cookie,一般都设置为http-only
,禁止js读取,这也是防止xss攻击的主要手段。
综上所述,对于存放CSRF令牌的cookie应该取消http-only
的设置,同时session_id
cookie默认要启用http-only
属性,此时后端采用通过头部验证令牌的方式才能全面的防范CSRF攻击。
备注:
- axios在发送POST请求时,如果当前域名存在
XSRF-TOKEN
cookie,它自动会构造好X-XSRF-TOKEN
头部。 - 关于
XSRF-TOKEN
,session_id
这些cookie的名字,不同的框架可能会有所不同,但是功能都是一样的。
完!
参考资料:
MDN - Cookies
laravel - CSRF Protection
laravel的CSRF防护机制和延伸