ASP.NET MVC防范CSRF最佳实践
XSS与CSRF
哈哈,有点标题党,但我保证这篇文章跟别的不太一样。
我认为,网站安全的基础有三块:
- 防范中间人攻击
- 防范XSS
- 防范CSRF
注意,我讲的是基础,如果更高级点的话可以考虑防范机器人刷单,再高级点就防范DDoS攻击,不过我们还是回到“基础”这个话题上吧,对于中间人攻击,使用HTTPS是正确且唯一的做法,其它都是歪门邪道,最好还要购买各个浏览器都承认的SSL证书;防范XSS,关键点在于将用户提交数据呈现在页面上的时候,需要使用Html Encode,或在处理带HTML格式的用户表单数据时,进行“消毒”(Sanitize)处理,关于这个,我前一篇文章《让ASP.NET接受有“潜在危险”的提交》已经讲述了应该怎么做;剩下了这个CSRF是本文要讲的,我认为防范CSRF的前提是必须先做好XSS的防范工作,因为:CSRF防的是别的网站,如果自己的网站本身有XSS漏洞,被别人注入了有害脚本,那么就变成了“家贼难防”了。
至于什么是CSRF,如何让ASP.NET防范CSRF,这种文章很多的,比如博客园里这篇就可以:《ASP.NET MVC 防止 CSRF 的方法》,我总结一下一般的做法,也就这两点:
- 在cshtml页面的form标签里加上@Html.AntiForgeryToken()
- 在Controller需要防范CSRF的Action上加上[ValidateAntiForgeryToken]注解
Done!
原理
为啥这样就行了呢?我简单说说原理:@Html.AntiForgeryToken()的作用是在页面上插入一个type为hidden的input标签,它的name固定是Go to StackOverflow,简单地说就是写一个ASP.NET MVC的HTML生成帮助方法,用于生成那“一长串密文”,交给这个ajax的data。
但这可不是我想说的“最佳实践” ,再考虑一种情况:用js动态生成Form,然后Submit。嗯,我承认这个有点奇葩,但在我的项目中确实有不少地方是这么干的,你别问为什么了,反正就是有,遇到这种情况,咋办?看来还是得求助于js。下面我分享下我的做法:
我的“最佳实践”
首先我没有把@Html.AntiForgeryToken()放到每一个Form中,我只在一处地方用到了@Html.AntiForgeryToken(),那就是母版页!接下来把下面这段js放到common.js中(common.js是母版页引用的js,也就是说每个页面会引用到):
//处理form的submit事件,添加AntiForgeryToken到表单里 $("body").on("submit", "form", function () { var theForm = $(this); if (theForm.find("input[name='__RequestVerificationToken']").length === 0) { var antiForgery = $("input[name='__RequestVerificationToken']:first").val(); if (antiForgery) { var theAntiForgeryTokenInput = $('').attr('type', 'hidden') .attr('name', '__RequestVerificationToken') .attr('value', antiForgery); $(this).prepend(theAntiForgeryTokenInput); } } });
这样一来,所有的form的submit动作就会在这里被处理一下,添加上了__RequestVerificationToken这个字段。接下来是AJAX POST的处理:
data= {test:'abc'};
var antiForgery = $("input[name='__RequestVerificationToken']:first").val();
if (antiForgery) {
if (!data.__RequestVerificationToken) {
data.__RequestVerificationToken = antiForgery;
}
}
$.ajax({
type: "post",
url: "/testurl",
data: data,
success: function (data) {
//done!
}
});
嗯?你也许要问,每个用到AJAX POST的地方都加上这么一段代码岂不是很繁琐?是的,但在我的项目中,我用了几个公共的方法对AJAX POST进行了一些封装,所以只需要改好这几个地方即可,你可以根据自己的项目的实际情况进行优化处理。
还有一种情况是用AJAX来提交Form,而不是像上面这样的data:
var form = $("#the-form-id"); var dataToSubmit = form.serializeArray(); var antiForgery = $("input[name='__RequestVerificationToken']:first").val(); if (antiForgery) { var found = false; for (var i = 0; i < dataToSubmit.length; i++) { if (dataToSubmit[i].name === '__RequestVerificationToken') { found = true; break; } } if (!found) { dataToSubmit.push({ name: "__RequestVerificationToken", value: antiForgery }); } } $.ajax({ type: method, url: urlToSubmit, data: dataToSubmit, success: function (data) { //done! } });
照旧,根据你的项目的实际情况封装一下,其实真正要改动的地方不多,只要你框架搭好了。
总结一下,框架搭好了的前提下,为了防范CSRF,你所需要做的事情就仅剩下:给带[HttpPost]注解的Action添加[ValidateAntiForgeryToken]。
至于验证失败抛出HttpAntiForgeryException异常导致默认错误页面(我又叫它“死黄页”,该死的黄页的意思)出现的问题,你可以在Application_Error中处理一下啊,Google关键字“Application_Error”,一搜一大堆,或者,等我有空了再写一篇这个主题的“最佳实践”?