CSRF是Web应用中相对复杂、且防御代价较大的一种攻击方式。
相关资料:
比如在A网站中,将100元支付给张三的请求是http://mysite.com/pay?to=zhangsan&ammount=100
。攻击者在第三方网站创建一个类似的链接,诱使A网站的用户点击这个链接,如果A网站的用户刚好已经登录A网站,就会触发转账请求。更糟糕的是,你可能不需要点击任何链接,就已经被攻击了。比如将这个地址作为图片的src,如<img src='http://mysite.com/pay?to=zhangsan&ammount=100'/>
。这就是所谓的隐式攻击,危害更大。
这时候很容易想到的防范措施是,不要接受任何以GET方式发起的付款请求。确实,这是一个基本的安全准则,GET方式只能用于获取数据,不能用于修改数据,修改数据要使用POST、PUT、DELETE等方式。但这并不能彻底防范CSRF攻击,攻击者可以创建一个iframe,在iframe里面创建一个method为POST的form,达到隐式攻击的目的。但这很明显的提高了攻击门槛,因为很多网站会允许用户发布链接(例如通过评论功能),却不会允许用户使用js创建一个iframe。
由于同源策略,使用xmlhttp(也就是ajax)无法发起站外攻击。除了链接和form表单,能够向不同域名发起请求的有<img> <script> <frame> <iframe>
。
需要注意避免攻击者使用<script>
、CORS
和JSONP
获取到敏感数据,如下面介绍的Token
值。
站外攻击需要用户已经登录了被攻击的网站,这需要一些巧合。而站内攻击则更容易实现这点。站内攻击需要结合XSS漏洞,或者能够发布链接甚至js编写权限。
GET方式用于获取数据,POST方式用于修改数据。使用POST修改数据,可以提高攻击的门槛,甚至避免一些攻击。
对于站外攻击,这是代价最小的防御方式。即这个请求必须是同源(如上例中的mysite.com
域名)发出来的请求,否则不予执行。
缺点在于有些用户会在禁止浏览器在发送请求时包含Referer,认为这样会暴露隐私,从而使得正常的请求也会被认为是CSRF攻击。另外IE6和FireFox2可以修改Referer,当然这不是个问题,现在几乎不会有人用这些已经淘汰的浏览器。
站内攻击由于是同源,无法使用检查的Referer的方式防范。
在服务端生成一个无法预测的随机字符串(如UUID),将这个字符串保存在Session或Cookie里。每个修改数据的请求(非GET请求,如POST,PUT,DELETE,PATCH),必须在Header或Parameter中附带这个Token值,否则不予执行。GET请求不能附带Token,因为Referer信息会泄露GET请求的Token。异源域名虽然可以通过 img script iframe 等标签提交请求,但却无法读取到响应的内容,无法获取到Token值,所以无法伪造成合法请求。
这个Token值保存在Cookie里相对没那么安全,因为Header(比如指定Cookie)能够被其它域的请求修改(使用flash10.2和307重定向);并且在cookie被泄露后,无法进行失效处理。
但对于绝大部分应用来说,放在Cookie里已经足够好了,新版本的flash也应该修复了相应漏洞,并且flash将于2020年停止更新,正逐步退出历史舞台。在微软ASP.NET Web文档里的做法就是将Token放到Cookie里。放在Session里则会存在过期的问题,对软件和开发使用带来更大影响,代价过大。
Token存储方式的三种方式:
站内攻击防御的三个级别:
使用验证Token的方式改变了通常的编程习惯,每个涉及修改数据的操作都需要加上一个Token值。对于一个已完成的系统,修改的工作量是巨大的。如果使用Session存储Token还存在Session过期的问题,对使用带来一定困扰。防御CSRF导致软件的开发和使用都付出极大的代价。
对于已开发好的系统,站外攻击使用检查Referer,站内链接攻击禁止GET方式修改数据并杜绝XSS漏洞,是防御CSRF攻击代价最小的方式。
新开发的系统还是使用验证Token的方式较为妥当,使用Cookie存储Token,取得足够的安全性的同时开发代价也相对较低。
使用Filter过滤所有非GET请求,如在Header或Parameter里不存在Token,则不予执行。并将Token保存在Cookie和Request中,便于页面调用。
表单提交代码:
<form action="..." method="post">
...
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
...
</form>
AJAX提交代码:
// AJAX 请求全部在 Header 中加上 CSRF TOKEN。需要js-cookie组件用于获取Cookie。
$(function () {
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader("X-XSRF-TOKEN", Cookies.get("XSRF-TOKEN"));
});
});