神刀安全网

防御CSRF的第四种方法

我们知道,一般防御CSRF有三种方法,判断referer、验证码、token。

对于判断referer来说,虽然客户端带用户状态的跨域提交,js和as已经无法伪造referer了;但是对于客户端软件和flash的提交,一般是不带referer的,据说一些山寨浏览器也不带。那么就需要为此开绿灯,但这样使得外站的flash请求伪造无法被防御。

而验证码弊端明显:会对用户造成影响。

token的问题也有一些:时效性无法保证;大型服务时,需要一台token生成及校验服务器;需要更改所有表单添加字段。

而最近我在做之类的防御时,想出了另外一种方法,跟xeye、woyigui等人在群里讨论了一番,认为应该是可行的,所以拿出来分享一下,并让其他的牛人看看是否有什么弊端

其实原理非常简单,与token也差不多:当表单提交时,用js在本域添加一个临时的cookies字段,并将过期时间设为1秒钟之后,然后再提交;服务端校验有这个字段即放行,没有则认为是CSRF。

token防csrf的原理是:无法通过ajax等方式获得外域页面中的token值,xmlhttprequest需要遵守浏览器同源策略;而临时cookies的原理也是:cookies只能在父域和子域之间设置,也遵守同源策略。

我们可以简单看一个demo:

demo:

http://127.0.0.1/test.html:

<script>
function doit(){
var expires = new Date((new Date()).getTime()+1000);
document.cookie = “xeye=xeye; expires=” + expires.toGMTString();
}
</script>
<form action=”http://127.0.0.1/test.php” name=”f” id=”f” onsubmit=”doit();” target=”if1″>
<input type=”button” value=”normal submit” onclick=”f.submit();”>
<input type=”button” value=”with token” onclick=”doit();f.submit();”>
<input type=”submit” value=”hook submit”>
</form>
<iframe src=”about:blank” name=”if1″ id=”if1″></iframe>

http://127.0.0.1/test.php

<?php
echo “<div>Cookies</div>”;
var_dump($_COOKIE);
?>

test.html为浏览器端的表单,里面有三个按钮:

第一个是正常的表单提交;第二个是添加临时cookies后提交表单;第三个是以hook submit事件来添加临时cookies并提交。

结果就像开头的图片演示那样,正常的表单提交不会出现临时cookies字段,第二个和第三个按钮提交则会出现。大家可以反复点击按钮来查看结果,但需要注意时间间隔需超过1秒。(当然可以将test.html拿到外域看看,不过要注意form的target不能指向iframe了,可以以新窗口打开。由于同源策略,cookies肯定是带不过去的)

不过这种方式只适用于单域名站点,或者安全需求不需要“当子域发生XSS隔离父域”。因为子域是可以操作父域的cookies的,所以它的缺陷也比较明显:这种方法无法防御由于其他子域产生的xss所进行的表单伪造提交。而一个区分分域的自校验token是可以防止从其他子域到本域的提交的。但如果对于单域而言,这种方法应该是足够的,并且安全性可能会略大于token。

和群里的几位大牛讨论了一下,也认为这种方式没有什么大问题,不过确实有一些小的疑问,譬如:

网络不流畅,有延迟会不会导致cookies失效。这个显然是不会的,因为服务端cookies是在提交请求的header中获得的。延时在服务端,不在客户端,而1秒钟足可以完成set Cookies+post header整个post表单的过程。

cookies的生成依赖于js,相当于这个token是明文的?这个的确,不管采取多少种加密,只要在客户端,就会被破解,不过不管怎样,csrf无法在有用户状态的情况下去添加这个临时cookies字段。虽然服务端curl等可以,但是无法将当前用户的状态也带过去。

外站是否可以伪造这个临时cookies呢?目前来看至少通过as和js无法向其他域添加和更改cookies的,通过服务端虽然可以伪造cookies,但获得不到目标域的用户状态。

如果目标域有XSS就完蛋了?恩,不过一般来说判断referer、token和简单的验证码(利用canvas识别?)也差不多完蛋了。

如果由于某种网络问题,获得不到cookies了呢?那么用户状态也不能获得了,用户只能再提交一次了。

ok,就这些!

说实话,这种新方法究竟是否真正有效我也没谱,说不定有某种BT的方式可以绕过?所以share一下,大家不妨看看是否真的有效。如果真有效,那么大概是一种最简单的,对代码改动最小,对服务器压力也最小的防御CSRF的方法了。

 

很巧妙的想法,但是没法100%保证不被攻击,这个方法的安全性依赖于cookie的失效时间,假设有个csrf页面里死循环不停提交的话,如果被攻击用户打开这个页面没有关闭,这个过程中如果用户又去被攻击的网站作一次正常操作的话,攻击就有一定的概率成功了,比如以下代码在ff中就有一定的概率成功(ie不行,因为iframe跨域提交cookie被阻止了):

<html>
<body>
<iframe name=”iframe1″></iframe>
<form id=”form1″ action=”http://127.0.0.1/test.php” method=”post” target=”iframe1″>
<input type=”submit” />
</form>
<script>
setInterval(function() {
var form1 = document.getElementById(“form1”);
form1.submit();
}, 500);
</script>
</body>
</html>
所以如果攻击者使受害者在FF下不断刷页面并且小于等于500毫秒,那么显然这种方式就失效了。由于浏览器限制,我们不能将cookies的失效时间改成比1秒更小。但我们可以提交完立即就清除cookies,由于submit事件没有返回,所以我们依然只能用延时来搞定。

所以我们将服务端的test.php改成:

<?php
echo “<div>Cookies</div>”;
var_dump($_COOKIE);
if(isset($_COOKIE[“xeye”])){
echo “<script>try{top.document} catch(ex){alert(1);}</script>”;
}
?>

这样当外域加载且被设置cookies时,就会弹窗口,本域就不会。

将客户端的改成:

<script>
function doit(){
var expires = new Date((new Date()).getTime()+2000);
document.cookie = “xeye=xeye; expires=” + expires.toGMTString();

f.submit();

setTimeout(function(){
var expires = new Date((new Date()).getTime()-2000);
document.cookie = “xeye=xeye; expires=” + expires.toGMTString();
},1);

return false;
}
</script>
<form action=”http://127.0.0.1/test.php” name=”f” id=”f” onsubmit=”return doit();” target=”if1″>
<input type=”button” value=”normal submit” onclick=”f.submit();”>
<input type=”button” value=”with token” onclick=”doit();”>
<input type=”submit” value=”hook submit”>
</form>
<iframe src=”about:blank” name=”if1″ id=”if1″></iframe>

通过测试发现,IE和chrome是没有任何问题的!不过FF偶尔还是弹出窗口,其原因在于它处理setTimeout居然有延时,并且应该是在0.5~1秒钟左右,巨汗,不过概率已经小一些了!

并且在测试中发现FF对按钮的onclick事件后的代码执行顺序与onsubmit的代码执行顺序居然不一样,onclick的事件中可以先set cookies、再submit、再清cookies;但onsubmit居然是先set cookies、再清cookies、再submit,服了。所以通过非常蹩脚的方法,我们可以将概率改成更小,但依旧不完美:

<script>
function doit(submit){
var expires = new Date((new Date()).getTime()+2000);
document.cookie = “xeye=xeye; expires=” + expires.toGMTString();

f.submit();

if(/a/[-1]==a && !submit){ //FF并且不是onsubmit事件
var expires = new Date((new Date()).getTime()-2000);
document.cookie = “xeye=xeye; expires=” + expires.toGMTString();
}else{
setTimeout(function(){
var expires = new Date((new Date()).getTime()-2000);
document.cookie = “xeye=xeye; expires=” + expires.toGMTString();
},1);
}
return false;
}
</script>
<form action=”http://127.0.0.1/test.php” name=”f” id=”f” onsubmit=”return doit(true);” target=”if1″>
<input type=”button” value=”normal submit” onclick=”f.submit();”>
<input type=”button” value=”with token” onclick=”doit(false);”>
<input type=”submit” value=”hook submit”>
</form>
<iframe src=”about:blank” name=”if1″ id=”if1″></iframe>

因为如果用onsubmit事件触发,则依旧有一点的概率使得防御被绕过!

不知各位大牛是否有什么针对FF的JS技巧使得代码可以先set cookies、再submit、再unset cookies这样的顺序执行,而不是异步?这样如果不用setTimeout,那么问题就可以解决了!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » 防御CSRF的第四种方法

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮