1. 什么是跨站请求伪造(CSRF)javascript
CSRF(Cross-site request forgery跨站请求伪造,也被称为“One Click Attack”或者Session Riding,一般缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS很是不一样,而且攻击方式几乎相左。XSS利用站点内的信任用户,而CSRF则经过假装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击每每不大流行(所以对其进行防范的资源也至关稀少)和难以防范,因此被认为比XSS更具危险性。html
以上是来自百度百科的概念。下面各自举个简单的例子说明:java
XSS:假设没有预防XSS,我在文章评论区域输入:<script>while(1)alert("呵呵")</script> 而且成功提交。那么下次进来这个页面进来,就会不断弹出对话框,致使该页面没法被正常浏览;对此咱们在输出内容时,若是该内容是用户输入的,那么就应该进行Html Encode,如上面的脚本就会以普通文本的形式显示出来。不只如此,攻击脚本还能够读取用户cookie信息,作任何脚本能作的事情。ajax
CSRF:与XSS不一样,CSRF利用的当前信任用户,让用户不知不觉的“本身提交”数据。例如用户 B 在用户 A 的文章评论区域上传一张图片,如:浏览器
[img]http://images2015.cnblogs.com/blog/798800/201512/798800-20151230205214839-1087717627.jpg[/img]安全
而后把它改为:服务器
[img]http://Another/Forgery.html[/img]cookie
这是一张无效的图片,而且来自于另外一个站点。在 Forgery.html 中,伪造者建立一个表单,action指向要攻击的页面,当window.onload 执行时自动提交表单。mvc
那么用户 A 在访问该页面时,就会发起请求到Another/Forgery.html。因为Web的身份验证信息一般会保存在浏览器cookie中,而cookie每次都会随请求提交到服务器,因此这个请求会把cookie 和 Forgery.html 的表单信息一块儿提交到服务器。服务器对Cookie进行相关验证,而且认为这是一个正常的请求,执行相关操做。app
2. 模拟一次攻击
按照上面的思路,接下来咱们模拟一次攻击。新建一个mvc项目,主要有两个页面,一个页面用于显示用户姓名和评论,另外一个页面用于用户本身修改姓名。只是为了演示,这里咱们固定一个用户:张三。如:
public class CurrentUser { private static CurrentUser currentUser = new CurrentUser(){Name="张三"}; public string Name{get;set;} public static CurrentUser Current { get { return currentUser; } } }
显示页面为:
本站点的另外一个页面用于修改用户姓名:
<div> 修改用户信息: </div> <form action="/home/update" method="post"> <p> <label>用户名:</label> <input name="name" /> </p> <p> <input type="submit" /> </p> </form>
对应Action为Update,为了提升安全性,咱们给它标记一个[HttpPost]特性(实际状况这里会进行身份验证,而后根据用户id去修改信息)。如:
[HttpPost] public ActionResult Update(string name) { CurrentUser.Current.Name = name; return RedirectToAction("Index"); }
能够看到上面的评论区域有一个连接,来自另外一个站点,它的代码很简单,与咱们的修改页面相似,以下:
<body> <div> <form id="form" action="http://localhost:50025/home/update" method="post"> <input type="hidden" name="name" value="2b" /> </form> </div> </body> <script type="text/javascript"> window.onload = function () { document.getElementById("form").submit(); } </script>
能够看到,该页面表达的action指向了前面的站点的修改页面,而且在页面load完后,就会自动提交。当张三点击这个连接后,会发生什么呢?以下:
3. 如何防止
3.1 尽早防范
永远不要相信用户提交的数据。一般咱们会在前台和后台对用户的输入进行验证,确保数据的正确性和安全性。以上面的例子,若是用户上传一张图片,而后修改为.html的格式,那么应该不让它保存。咱们能够看博客园的例子,若是这样作,能够保存,但显示出来就是普通文本的格式,这样页面加载时就不会对这个url发起请求。再看csdn,则会弹出提示非法输入。
cnblogs:
csdn:
固然,这样只是第一道屏障。不少网站也可能像上面能够输入外部连接,若是用户去点击,依然可能被攻击。因此咱们还须要进一步防范。
3.2 MVC 的作法
前面咱们模拟了CSRF的过程,接下看MVC里如何应对这种状况。
ValidateAntiForgeryAttribute特性
这是一个继承了FilterAttribute 和 实现了 IActionFilter 的标记特性,它能够应用在Controller或者Action上面。咱们知道实现IActionFilter的 Filter会在 Action执行前进行相关处理,具体逻辑在 IActionFilter接口的 OnAuthorization 方法中。MVC 就是在这个方法中进行验证的。具体是如何验证的呢?
AntiForgeryToken方法
ValidateAntiForgery特性表示操做须要验证,咱们还须要使用HtmlHelper的 AntiForgeryToken方法,这是一个实例方法。具体是在View的表单里调用该方法,该方法会生成一个name为__RequestVerificationToken的 input hidden标签,值就是防伪令牌。除此以外,还会生成一个一样名称而且标记为HttpOnly的cookie,值也是经过加密生成的防伪令牌。ValidateAntiForgery特性的OnAuthorization方法就是根据这两个进行验证的。具体是:
1. 用户请求该页面,AntiForgeryToken方法会生成一个input hidden 和 cookie,值都是通过加密处理的Token。
2. 用户提交请求,若是Action(Controller)标记了 ValidateAntiForgery特性,则进行验证。
2.1 若是表单没有一个name为 __RequestVerificationToken的元素,则抛出HttpAntiForgeryException。
2.2 若是没有一个name为__RequestVerificationToken的cookie,则抛出HttpAntiForgeryException。
2.3 解析input 和 cookie 的值,判断是否匹配(包括用户名、时间等的比较),不匹配则抛出HttpAntiForgeryException。
3. 接收到异常,显示错误页或抛出黄页。
至于 input 的值 和 cookie 的生成,mvc内部会根据当前用户名,时间以及集合 MachineKey 等去加密生成,确保不会轻易被猜出。有兴趣的朋友能够经过源码了解详细过程。
按照上面的作法,咱们给Update加上一个[ValidateAntiForgery]特性,而且在表单调用HtmlHelper的AntiForgeryToken方法。此时若是用户点击连接,同样访问了Forgery.html,而且自动提交表单,cookie仍是同样会提交,但伪造页面没法知道input hidden 的值,因此没法经过验证。
3.3 WebForm 的作法
WebForm 没有 AntiForgeryToken方法 能够直接使用,不过知道MVC的实现过程后,咱们也能够本身实现一套。
在页面表单,像mvc同样,咱们也输出一个名称为:_RequestVerificationToken 的 input hidden 标签,值为序列化后的Token,具体是调用 HttpRespose 的扩展方法AntiForgeryToken。AntiForgeryToken方法不只会输入input hidden,还会将Guid存储在Context.Item,这是一个在一次请求内各个时期可使用的集合,在页面周期完成后,咱们判断是否有这个标记,若是有,还须要将它写入到Cookie当中。
表单:
<form id="Form1" action="UpdateAntiCsrf.aspx" method="post" runat="server"> <div> <%=Response.AntiForgeryToken() %> <input type="text" name="name"/> <input type="submit"/> </div> </form>
AntiForgeryToken扩展方法:
public static class HttpResposeExtentions { public static string AntiForgeryToken(this HttpResponse response) { HttpContext context = HttpContext.Current; if (context == null) { throw new InvalidOperationException("无效请求!"); } Guid guid = Guid.NewGuid(); context.Items["_RequestVerificationToken"] = guid; ObjectStateFormatter formatter = new ObjectStateFormatter(); return string.Format("<input type='hidden' name='_RequestVerificationToken' value={0} />", formatter.Serialize(guid)); } }
对于验证Token,和将GUID写入到Cookie是经过一个AntiCsrfModule完成的,它主要拦截页面执行前和执行后两个事件。页面执行后完成上面是否须要将GUID写入Cookie的判断,而页面执行前则判断是否须要验证,以及验证结果,一旦不匹配,就抛出异常。代码以下:
public class AntiCsrfModule : IHttpModule { public void Dispose () { } public void Init(HttpApplication app) { app.PreRequestHandlerExecute += new EventHandler(app_PreRequestHandlerExecute); app.PostRequestHandlerExecute += new EventHandler(app_PostRequestHandlerExecute); } void app_PreRequestHandlerExecute(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; HttpRequest request = context.Request; IHttpHandler handler = context.Handler; if (handler.GetType().IsDefined(typeof(ValidationAntiForgeryAttribute), true)) { if (request.HttpMethod.Equals("POST", StringComparison.CurrentCultureIgnoreCase)) { HttpCookie cookie = request.Cookies["_RequestVerificationToken"]; if (cookie == null) { throw new InvalidOperationException("无效请求!"); } string value = request.Form["_RequestVerificationToken"]; if (string.IsNullOrEmpty(value)) { throw new InvalidOperationException("无效请求!"); } ObjectStateFormatter formatter = new ObjectStateFormatter(); Guid? guid = formatter.Deserialize(value) as Guid?; if(guid.HasValue && guid.Value.ToString() == cookie.Value) { return; } throw new InvalidOperationException("无效请求!"); } } } void app_PostRequestHandlerExecute(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; Guid? guid = context.Items["_RequestVerificationToken"] as Guid?; if (guid.HasValue) { HttpCookie cookie = new HttpCookie("_RequestVerificationToken", guid.Value.ToString()); cookie.HttpOnly = false; context.Response.Cookies.Add(cookie); } } }
对于须要验证的页面,经过一个ValidateAntiForgeryAttribute特性标记,以下:
public class ValidateAntiForgeryAttribute : Attribute { }
一样,咱们像前面同样模拟一次攻击。结果如咱们所想,会抛出黄页。
3.4 Ajax 方式
上面咱们都是经过Post 表单的形式提交数据,若是是以ajax提交的呢?咱们能够在后台判断请求是不是Ajax请求,若是不是则不容许操做。由于js受同源策略限制,另外一个域在没有被受权的状况下,脚本是没法和本域进行通讯的。也就是Another/Forgery.html能够以post的形式提交数据到咱们后台,但没办法以ajax的形式提交,也没办法调用咱们页面的方法或者访问dom元素。
4. 博客园的实现
例子就在身边。咱们看到博客园【设置基本资料】模块,查看源码就会发现这里用用到了这个技术。
表单:
Cookie: