网站安全之——CSRF攻击

转自:http://www.ibm.com/developerworks/cn/web/1102_niugang_csrf/

转自:https://github.com/astaxie/build-web-application-with-golang/blob/master/ebook/09.1.md

CSRF(CrossSiteRequestForgery,跨站域请求伪造)是一种网络的攻击方式,该攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作,有很大的危害性。然而,该攻击方式并不为大家所熟知,很多网站都有CSRF的安全漏洞。本文首先介绍CSRF的基本原理与其危害性,然后就目前常用的几种防御方法进行分析,比较其优劣。最后,本文将以实例展示如何在网站中防御CSRF的攻击,并分享一些开发过程中的最佳实践。

5评论:

牛刚,软件工程师,IBM

童强国,高级软件工程师,IBM

2011年2月24日

+

内容

CSRF背景与介绍

CSRF(CrossSiteRequestForgery,跨站域请求伪造)是一种网络的攻击方式,它在2007年曾被列为互联网20大安全隐患之一。其他安全隐患,比如SQL脚本注入,跨站域脚本攻击等在近年来已经逐渐为众人熟知,很多网站也都针对他们进行了防御。然而,对于大多数人来说,CSRF却依然是一个陌生的概念。即便是大名鼎鼎的Gmail,在2007年底也存在着CSRF漏洞,从而被黑客攻击而使Gmail的用户造成巨大的损失。

CSRF攻击实例

CSRF攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作。比如说,受害者Bob在银行有一笔存款,通过对银行的网站发送请求http://bank.example/withdraw?account=bob&amount=1000000&for=bob2可以使Bob把1000000的存款转到bob2的账号下。通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的session,并且该session的用户Bob已经成功登陆。黑客Mallory自己在该银行也有账户,他知道上文中的URL可以把钱进行转帐操作。Mallory可以自己发送一个请求给银行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory。但是这个请求来自Mallory而非Bob,他不能通过安全认证,因此该请求不会起作用。这时,Mallory想到使用CSRF的攻击方式,他先自己做一个网站,在网站中放入如下代码:src=”http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory”,并且通过广告等诱使Bob来访问他的网站。当Bob访问该网站时,上述url就会从Bob的浏览器发向银行,而这个请求会附带Bob浏览器中的cookie一起发向银行服务器。大多数情况下,该请求会失败,因为他要求Bob的认证信息。但是,如果Bob当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的session尚未过期,浏览器的cookie之中含有Bob的认证信息。这时,悲剧发生了,这个url请求就会得到响应,钱将从Bob的账号转移到Mallory的账号,而Bob当时毫不知情。等以后Bob发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而Mallory则可以拿到钱后逍遥法外。

CSRF攻击的对象

在讨论如何抵御CSRF之前,先要明确CSRF攻击的对象,也就是要保护的对象。从以上的例子可知,CSRF攻击是黑客借助受害者的cookie骗取服务器的信任,但是黑客并不能拿到cookie,也看不到cookie的内容。另外,对于服务器返回的结果,由于浏览器同源策略的限制,黑客也无法进行解析。因此,黑客无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。所以,我们要保护的对象是那些可以直接产生数据改变的服务,而对于读取数据的服务,则不需要进行CSRF的保护。比如银行系统中转账的请求会直接改变账户的金额,会遭到CSRF攻击,需要保护。而查询余额是对金额的读取操作,不会改变数据,CSRF攻击无法解析服务器返回的结果,无需保护。

回页首

当前防御CSRF的几种策略

在业界目前防御CSRF攻击主要有三种策略:验证HTTPReferer字段;在请求地址中添加token并验证;在HTTP头中自定义属性并验证。下面就分别对这三种策略进行详细介绍。

验证HTTPReferer字段

根据HTTP协议,在HTTP头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的Referer值就会是转账按钮所在的页面的URL,通常是以bank.example域名开头的地址。而如果黑客要对银行网站实施CSRF攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的Referer是指向黑客自己的网站。因此,要防御CSRF攻击,银行网站只需要对于每一个转账请求验证其Referer值,如果是以bank.example开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果Referer是其他网站的话,则有可能是黑客的CSRF攻击,拒绝该请求。

这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心CSRF的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查Referer的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。

然而,这种方法并非万无一失。Referer的值是由浏览器提供的,虽然HTTP协议上有明确的要求,但是每个浏览器对于Referer的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证Referer值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如IE6或FF2,目前已经有一些方法可以篡改Referer值。如果bank.example网站支持IE6浏览器,黑客完全可以把用户浏览器的Referer值设为以bank.example域名开头的地址,这样就可以通过验证,从而进行CSRF攻击。

即便是使用最新的浏览器,黑客无法篡改Referer值,这种方法仍然有问题。因为Referer值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心Referer值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供Referer。当他们正常访问银行网站时,网站会因为请求没有Referer值而认为是CSRF攻击,拒绝合法用户的访问。

在请求地址中添加token并验证

CSRF攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于cookie中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的cookie来通过安全验证。要抵御CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于cookie之中。可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务器端建立一个拦截器来验证这个token,如果请求中没有token或者token内容不正确,则认为可能是CSRF攻击而拒绝该请求。

这种方法要比检查Referer要安全一些,token可以在用户登陆后产生并放于session之中,然后在每次请求时把token从session中拿出,与请求中的token进行比对,但这种方法的难点在于如何把token以参数的形式加入请求。对于GET请求,token将附在请求地址之后,这样URL就变成http://url?csrftoken=tokenvalue。而对于POST请求来说,要在form的最后加上<inputtype=”hidden”name=”csrftoken”value=”tokenvalue”/>,这样就把token以参数的形式加入请求了。但是,在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上token是很麻烦的,并且很容易漏掉,通常使用的方法就是在每次页面加载时,使用javascript遍历整个dom树,对于dom中所有的a和form标签后加入token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的html代码,这种方法就没有作用,还需要程序员在编码时手动添加token。

该方法还有一个缺点是难以保证token本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址。由于系统也会在这个地址后面加上token,黑客可以在自己的网站上得到这个token,并马上就可以发动CSRF攻击。为了避免这一点,系统可以在添加token的时候增加一个判断,如果这个链接是链到自己本站的,就在后面添加token,如果是通向外网则不加。不过,即使这个csrftoken不以参数的形式附加在请求之中,黑客的网站也同样可以通过Referer来得到这个token值以发动CSRF攻击。这也是一些用户喜欢手动关闭浏览器Referer功能的原因。

在HTTP头中自定义属性并验证

这种方法也是使用token并进行验证,和上一种方法不同的是,这里并不是把token以参数的形式置于HTTP请求之中,而是把它放到HTTP头中自定义的属性里。通过XMLHttpRequest这个类,可以一次性给所有该类请求加上csrftoken这个HTTP头属性,并把token值放入其中。这样解决了上种方法在请求中加入token的不便,同时,通过XMLHttpRequest请求的地址不会被记录到浏览器的地址栏,也不用担心token会透过Referer泄露到其他网站中去。

然而这种方法的局限性非常大。XMLHttpRequest请求通常用于Ajax方法中对于页面局部的异步刷新,并非所有的请求都适合用这个类来发起,而且通过该类请求得到的页面不能被浏览器所记录下,从而进行前进,后退,刷新,收藏等操作,给用户带来不便。另外,对于没有进行CSRF防护的遗留系统来说,要采用这种方法来进行防护,要把所有请求都改为XMLHttpRequest请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。

回页首

Java代码示例

下文将以Java为例,对上述三种方法分别用代码进行示例。无论使用何种方法,在服务器端的拦截器必不可少,它将负责检查到来的请求是否符合要求,然后视结果而决定是否继续请求或者丢弃。在Java中,拦截器是由Filter来实现的。我们可以编写一个Filter,并在web.xml中对其进行配置,使其对于访问所有需要CSRF保护的资源的请求进行拦截。

在filter中对请求的Referer验证代码如下

清单1.在Filter中验证Referer

//从HTTP头中取得Referer值

Stringreferer=request.getHeader("Referer");

//判断Referer是否以bank.example开头

if((referer!=null)&&(referer.trim().startsWith(“bank.example”))){

chain.doFilter(request,response);

}else{

request.getRequestDispatcher(“error.jsp”).forward(request,response);

}

以上代码先取得Referer值,然后进行判断,当其非空并以bank.example开头时,则继续请求,否则的话可能是CSRF攻击,转到error.jsp页面。

如果要进一步验证请求中的token值,代码如下

清单2.在filter中验证请求中的token

HttpServletRequestreq=(HttpServletRequest)request;

HttpSessions=req.getSession();

//从session中得到csrftoken属性

StringsToken=(String)s.getAttribute(“csrftoken”);

if(sToken==null){

//产生新的token放入session中

sToken=generateToken();

s.setAttribute(“csrftoken”,sToken);

chain.doFilter(request,response);

}else{

//从HTTP头中取得csrftoken

StringxhrToken=req.getHeader(“csrftoken”);

//从请求参数中取得csrftoken

StringpToken=req.getParameter(“csrftoken”);

if(sToken!=null&&xhrToken!=null&&sToken.equals(xhrToken)){

chain.doFilter(request,response);

}elseif(sToken!=null&&pToken!=null&&sToken.equals(pToken)){

chain.doFilter(request,response);

}else{

request.getRequestDispatcher(“error.jsp”).forward(request,response);

}

}

首先判断session中有没有csrftoken,如果没有,则认为是第一次访问,session是新建立的,这时生成一个新的token,放于session之中,并继续执行请求。如果session中已经有csrftoken,则说明用户已经与服务器之间建立了一个活跃的session,这时要看这个请求中有没有同时附带这个token,由于请求可能来自于常规的访问或是XMLHttpRequest异步访问,我们分别尝试从请求中获取csrftoken参数以及从HTTP头中获取csrftoken自定义属性并与session中的值进行比较,只要有一个地方带有有效token,就判定请求合法,可以继续执行,否则就转到错误页面。生成token有很多种方法,任何的随机算法都可以使用,Java的UUID类也是一个不错的选择。

除了在服务器端利用filter来验证token的值以外,我们还需要在客户端给每个请求附加上这个token,这是利用js来给html中的链接和表单请求地址附加csrftoken代码,其中已定义token为全局变量,其值可以从session中得到。

清单3.在客户端对于请求附加token

functionappendToken(){

updateForms();

updateTags();

}

functionupdateForms(){

//得到页面中所有的form元素

varforms=document.getElementsByTagName('form');

for(i=0;i<forms.length;i++){

varurl=forms[i].action;

//如果这个form的action值为空,则不附加csrftoken

if(url==null||url=="")continue;

//动态生成input元素,加入到form之后

vare=document.createElement("input");

e.name="csrftoken";

e.value=token;

e.type="hidden";

forms[i].appendChild(e);

}

}

functionupdateTags(){

varall=document.getElementsByTagName('a');

varlen=all.length;

//遍历所有a元素

for(vari=0;i<len;i++){

vare=all[i];

updateTag(e,'href',token);

}

}

functionupdateTag(element,attr,token){

varlocation=element.getAttribute(attr);

if(location!=null&&location!=''''){

varfragmentIndex=location.indexOf('#');

varfragment=null;

if(fragmentIndex!=-1){

//url中含有只相当页的锚标记

fragment=location.substring(fragmentIndex);

location=location.substring(0,fragmentIndex);

}

varindex=location.indexOf('?');

if(index!=-1){

//url中已含有其他参数

location=location+'&csrftoken='+token;

}else{

//url中没有其他参数

location=location+'?csrftoken='+token;

}

if(fragment!=null){

location+=fragment;

}

element.setAttribute(attr,location);

}

}

在客户端html中,主要是有两个地方需要加上token,一个是表单form,另一个就是链接a。这段代码首先遍历所有的form,在form最后添加一隐藏字段,把csrftoken放入其中。然后,代码遍历所有的链接标记a,在其href属性中加入csrftoken参数。注意对于a.href来说,可能该属性已经有参数,或者有锚标记。因此需要分情况讨论,以不同的格式把csrftoken加入其中。

如果你的网站使用XMLHttpRequest,那么还需要在HTTP头中自定义csrftoken属性,利用dojo.xhr给XMLHttpRequest加上自定义属性代码如下:

清单4.在HTTP头中自定义属性

varplainXhr=dojo.xhr;

//重写dojo.xhr方法

dojo.xhr=function(method,args,hasBody){

//确保header对象存在

args.headers=args.header||{};

tokenValue='<%=request.getSession(false).getAttribute("csrftoken")%>';

vartoken=dojo.getObject("tokenValue");

//把csrftoken属性放到头中

args.headers["csrftoken"]=(token)?token:"";

returnplainXhr(method,args,hasBody);

};

这里改写了dojo.xhr的方法,首先确保dojo.xhr中存在HTTP头,然后在args.headers中添加csrftoken字段,并把token值从session里拿出放入字段中。

回页首

CSRF防御方法选择之道

通过上文讨论可知,目前业界应对CSRF攻击有一些克制方法,但是每种方法都有利弊,没有一种方法是完美的。如何选择合适的方法非常重要。如果网站是一个现有系统,想要在最短时间内获得一定程度的CSRF的保护,那么验证Referer的方法是最方便的,要想增加安全性的话,可以选择不支持低版本浏览器,毕竟就目前来说,IE7+,FF3+这类高版本浏览器的Referer值还无法被篡改。

如果系统必须支持IE6,并且仍然需要高安全性。那么就要使用token来进行验证,在大部分情况下,使用XmlHttpRequest并不合适,token只能以参数的形式放于请求之中,若你的系统不支持用户自己发布信息,那这种程度的防护已经足够,否则的话,你仍然难以防范token被黑客窃取并发动攻击。在这种情况下,你需要小心规划你网站提供的各种服务,从中间找出那些允许用户自己发布信息的部分,把它们与其他服务分开,使用不同的token进行保护,这样可以有效抵御黑客对于你关键服务的攻击,把危害降到最低。毕竟,删除别人一个帖子比直接从别人账号中转走大笔存款严重程度要轻的多。

如果是开发一个全新的系统,则抵御CSRF的选择要大得多。笔者建议对于重要的服务,可以尽量使用XMLHttpRequest来访问,这样增加token要容易很多。另外尽量避免在js代码中使用复杂逻辑来构造常规的同步请求来访问需要CSRF保护的资源,比如window.location和document.createElement(“a”)之类,这样也可以减少在附加token时产生的不必要的麻烦。

最后,要记住CSRF不是黑客唯一的攻击手段,无论你CSRF防范有多么严密,如果你系统有其他安全漏洞,比如跨站域脚本攻击XSS,那么黑客就可以绕过你的安全防护,展开包括CSRF在内的各种攻击,你的防线将如同虚设。

回页首

总结与展望

可见,CSRF是一种危害非常大的攻击,又很难以防范。目前几种防御策略虽然可以很大程度上抵御CSRF的攻击,但并没有一种完美的解决方案。一些新的方案正在研究之中,比如对于每次请求都使用不同的动态口令,把Referer和token方案结合起来,甚至尝试修改HTTP规范,但是这些新的方案尚不成熟,要正式投入使用并被业界广为接受还需时日。在这之前,我们只有充分重视CSRF,根据系统的实际情况选择最合适的策略,这样才能把CSRF的危害降到最低。

参考资料

学习

维基百科CSRF,这里对CSRF有一个较为全面地介绍。http://en.wikipedia.org/wiki/Cross-site_request_forgery

开源项目CSRFGuard,介绍了如何使用在HTTP请求中加入token并验证的方法来抵御CSRF。http://www.owasp.org/index.php/Category:OWASP_CSRFGuard_Project

PreventCross-siteRequestForgery:PCRF。建议了一种在PHP中使用token来抵御CSRF的方法。http://userweb.cs.utexas.edu/~samuel/PCRF/Final_PCRF_paper.pdf

RobustDefensesforCross-siteRequestForgery:分析了一些常用的CSRF抵御方法并独创性的提出增强浏览器的解决方案。

developerWorksWebdevelopment专区:通过专门关于Web技术的文章和教程,扩展您在网站开发方面的技能。

developerWorksAjax资源中心:这是有关Ajax编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki和新闻。任何Ajax的新信息都能在这里找到。

developerWorksWeb2.0资源中心,这是有关Web2.0相关信息的一站式中心,包括大量Web2.0技术文章、教程、下载和相关技术资源。您还可以通过Web2.0新手入门栏目,迅速了解Web2.0的相关概念。

查看HTML5专题,了解更多和HTML5相关的知识和动向。

讨论

加入developerWorks中文社区。

什么是CSRF

CSRF(Cross-siterequestforgery),中文名称:跨站请求伪造,也被称为:oneclickattack/sessionriding,缩写为:CSRF/XSRF。

那么CSRF到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过QQ等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使Web应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个QQ好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。

所以遇到CSRF攻击时,将对终端用户的数据和操作指令构成严重的威胁;当受攻击的终端用户具有管理员帐户的时候,CSRF攻击将危及整个Web应用程序。

CSRF的原理

下图简单阐述了CSRF攻击的思想

图9.1CSRF的攻击过程

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:

1.登录受信任网站A,并在本地生成Cookie。

2.在不退出A的情况下,访问危险网站B。

看到这里,读者也许会问:“如果我不满足以上两个条件中的任意一个,就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:

你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站,特别现在浏览器都是支持多tab的。

你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。

上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。

因此对于用户来说很难避免在登陆一个网站之后不点击一些链接进行其他操作,所以随时可能成为CSRF的受害者。

CSRF攻击主要是因为Web的隐式身份验证机制,Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。

如何预防CSRF

过上面的介绍,读者是否觉得这种攻击很恐怖,意识到恐怖是个好事情,这样会促使你接着往下看如何改进和防止类似的漏洞出现。

CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。

服务端的预防CSRF攻击的方式方法有多种,但思想上都是差不多的,主要从以下2个方面入手:

1、正确使用GET,POST和Cookie;

2、在非GET请求中增加伪随机数;

我们上一章介绍过REST方式的Web应用,一般而言,普通的Web应用都是以GET、POST为主,还有一种请求是Cookie方式。我们一般都是按照如下方式设计应用:

1、GET常用在查看,列举,展示等不需要改变资源属性的时候;

2、POST常用在下达订单,改变一个资源的属性或者做其他一些事情;

接下来我就以Go语言来举例说明,如何限制对资源的访问方法:

mux.Get("/user/:uid",getuser)

mux.Post("/user/:uid",modifyuser)

这样处理后,因为我们限定了修改只能使用POST,当GET方式请求时就拒绝响应,所以上面图示中GET方式的CSRF攻击就可以防止了,但这样就能全部解决问题了吗?当然不是,因为POST也是可以模拟的。

因此我们需要实施第二步,在非GET方式的请求中增加随机数,这个大概有三种方式来进行:

为每个用户生成一个唯一的cookietoken,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,所以这个方案必须要在没有XSS的情况下才安全。

每个请求使用验证码,这个方案是完美的,因为要多次输入验证码,所以用户友好性很差,所以不适合实际运用。

不同的表单包含一个不同的伪随机值,我们在4.4小节介绍“如何防止表单多次递交”时介绍过此方案,复用相关代码,实现如下:

生成随机数token

h:=md5.New()

io.WriteString(h,strconv.FormatInt(crutime,10))

io.WriteString(h,"ganraomaxxxxxxxxx")

token:=fmt.Sprintf("%x",h.Sum(nil))

t,_:=template.ParseFiles("login.gtpl")

t.Execute(w,token)

输出token

<inputtype="hidden"name="token"value="{{.}}">

验证token

r.ParseForm()

token:=r.Form.Get("token")

iftoken!=""{

//验证token的合法性

}else{

//不存在token报错

}

这样基本就实现了安全的POST,但是也许你会说如果破解了token的算法呢,按照理论上是,但是实际上破解是基本不可能的,因为有人曾计算过,暴力破解该串大概需要2的11次方时间。

总结

跨站请求伪造,即CSRF,是一种非常危险的Web安全威胁,它被Web安全界称为“沉睡的巨人”,其威胁程度由此“美誉”便可见一斑。本小节不仅对跨站请求伪造本身进行了简单介绍,还详细说明造成这种漏洞的原因所在,然后以此提了一些防范该攻击的建议,希望对读者编写安全的Web应用能够有所启发。