10.2 Web 厂商的防御
在此推荐的防御方案在具体实施的过程中,Web厂商需要仔细评估自己的业务场景,然后给出合理的安全架构方案。请牢记:业务第一,安全围绕业务合理地展开,不可或缺。
除了合理实施上面说的那些浏览器策略外,Web厂商还需要考虑如下情况。
10.2.1 域分离
域分离做得好的可以参考Google,Google将一些业务关联性小的内容转移到了不相干的域中,比如:
http://www.google.com/enterprise/mini/554_google_mini.swf
和广告有关系的内容,在访问的时候会跳转到:
http://static.googleusercontent.com/exter-nal_content/untrusted_dlcp/www.google.com/zh-CN//enterprise/mini/554_google_mini.swf
这个跳转方案是后面才加上的,这样的后续解决方案既保证了安全性,又不影响之前链接的有效性。如果googleusercontent.com域下出现了XSS,也不会影响到Google的业务主域,所以这个域下的XSS拿不到Google奖励的美金。
Google不同业务的子域都进行了上面这样安全的划分,且子域很严格,不会出现很多网站为了方便用统一的js文件设置子域的document.do-main为顶级域,这样,不同业务的子域在Web前端可以互相影响,比如,人人网、QQ很多服务的子域设计。
还有一种糟糕的子域设计是新浪微博,主内容都在顶级域下(weibo.com),大量的子域提供不同的业务,任何一个子域有XSS,都可以轻易跨到顶级域下。
同样,糟糕的设计还有,在重要的业务子域下面有一个类似proxy.html的文件,这个文件会将document.domain设置为顶级域,那么就可以很容易地被其他子域的XSS利用,而间接地跨到这个重要业务的子域上。
这些糟糕的子域设计带来的安全风险在第7章已经有很详细的介绍,在此省略。
10.2.2 安全传输
Google很多重要的业务都完美地支持HTTPS安全传输(包括搜索)。安全传输可以有效地防止局域网内的明文抓包。就这点就让人安心,虽然防御不了XSS。国内很多重要的服务认为只要登录口与账号设置页面是HTTPS就足够,这个观点是错误的,被嗅探时,这个方案泄漏不了密码,但是在非HTTPS页面上照样可以泄漏身份认证Cookie以及页面的明文信息。
还有的服务HTTPS全站似乎都做得很好,但是在WAP页面上却还是HTTP,这又是一个安全脆弱口。
10.2.3 安全的Cookie
在第2章详细介绍了Cookie安全,这方面的典型同样可以学学Google,某些身份认证相关的Cookie肯定严格设置为HTTPS传输,肯定是HttpOnly标志,这样XSS即使盗取了Cookie,也无法正确使用。
有的服务乍一看也支持HttpOnly Cookie,但是这样的Cookie却和身份认证关系不大,只能说错误地应用了HttpOnly。
10.2.4 优秀的验证码
验证码的出现肯定降低了用户体验,但是这个降低阈值是可以控制好的,比如,Google的验证码,在登录进入页面前几次是不出现的,只有登录失败几次后才会出现,且Google不同业务的验证码体系的同一套,不会像有的网站在不同业务之间出现不同风格的验证码,这样安全性很难保证一致。
还有一些重要页面,比如注册页,其中的验证码必须填写,目的就是为了防止大量非人类的注册。
Google的验证码公认是比较安全的(字母连着、扭曲变形、线条平滑、无噪等),暴力破解很困难,这也带来了用户体验上的尴尬,经常会输错验证码,说明Google非常重视安全,宁可牺牲一点用户体验,这对我们来说是可以接受的。
这样的验证码如果出现在一些功能重要的地方,是可以有效防止XSS/CSRF非法提交的,比如,对一些资料的篡改,在HTML 5中,虽然JavaScript canvas可以进行图片识别(同域内),破解验证码,但这个代价太高,真实的攻击中还未见到。
10.2.5 慎防第三方内容
第三方内容的安全性是经常被大家提起的,常见的有以下几种形式:
· <script>引用第三方js文件,比如,各种流行的统计脚本、广告脚本等。
· <iframe>引用第三方HTML文件,比如,各种广告、第三方应用等。
· <objet>等引用第三方Flash等资源,比如,各种广告、游戏等。
针对第1点,经典的案例是2009年初ring04h针对phpwind和Discuz!的劫持事件,通过对ph-pwind和Discuz!管理员后台<script>嵌入的统计脚本域名进行劫持,篡改了统计脚本的内容,导致登录管理员后台的管理员执行了恶意JavaScript代码,从而使论坛首页被篡改。
这个经典的案例对我们是一个很好的提醒,那些使用第三方js文件的网站需要好好评估一下,它们的安全性高吗?
针对第2点,曾经出现最多的案例就是挂马事件,包括新浪宠物频道,曾经就因为<iframe>嵌入的页面被挂马,直接导致访问新浪宠物频道的人中招。
对于第3点,有以下两个经典的案例。
一个是人人网于2009年曾经出现的Flash蠕虫,攻击者制作了一个Flash假视频植入人人网的个人分享中,这个Flash假视频可以执行恶意JavaScript代码,单击视频的人都会中招传播该恶意的Flash。
另一个是2008年发生在国外知名网站(MSNBC.COM)上的剪贴板劫持事件,大概过程是MSNBC.COM上有一个Flash广告,这个广告可能被攻击者控制,并植入了恶意的ActionScript代码,如下:
if (clipboard.length) System.setClipboard(clipboard);
当用户访问MSNBC.COM时,用户的剪贴板会自动植入下面的网址:
http://xp-vista-update.net/?id=xx
其中,id会根据值的不同,重定向到不同的网页,几乎都是做杀毒软件虚假推广的。后来Flash因为好几起这类事件也进行了更新,不允许自动设置剪贴板的内容,而需要用户交互(比如用户单击)才行。
由此可见,第三方内容带来的危害可以很大,Web厂商们需谨慎。
10.2.6 XSS防御方案
我们认为熟悉了“攻”,“防”才会更加顺手,否则对于只关注防的人来说,是很希望有一种手册般完美的防御策略的,这太理想化了。下面的防御方案中,我们会主要引入OWASP的XSS防御方案,原因如下:
· OWASP出品的质量是业界公认的,XSS防御方案虽然不够完美,但已经不错了。
· OWASP给出的解决方案不仅仅是文字,还有ESAPI(企业级安全API),几乎流行的语言都可以离线调用对应的接口进行相应的防御,这省去了我们自己写相关的过滤函数与防御逻辑。
下面介绍一些防御策略。首先假定一切输入都是有害的,那么在服务端的对策一般如下。
· 输入校验:长度限制、值类型是否正确、是否包含特殊字符(如<>'"等)。其实校验是对数据无害的,满足就放行,不满足就阻止。这个过程一般不建议进行任何过滤操作,这样能保证数据的原生态
· 输出编码:根据输出的位置进行相应的编码,如HTML编码、JavaScript编码、URL编码。原则就是:该数据不要超出自己所在的区域,也不要被当做指令执行。
下面列举OWASP XSS防御的一些规则,其中融入了我们的观点。
规则一:进入HTML标签之间时
比如,在<div></div>之间,HTML编码转换规则如下:
& -->& < -->< > -->> " -->" ' -->' '不被建议了 / -->/ 包含斜杆是因为它是HTML结束标签里的符号
使用ESAPI接口就非常简单了,一个函数调用(以下都以Java为例,其他语言类似)如下:
String safe = ESAPI.encoder().encodeForHTML( request.getParameter( "input" ) );
规则二:进入HTML普通属性值时
普通属性如value、width、height等,而href、src、action、style、on*事件等除外(参考规则三)。普通属性的样例如下:
<div attr=...编码不可信的数据...>content</div>无引号包围 <div attr='...编码不可信的数据...'>content</div>单引号包围着 <div attr="...编码不可信的数据...">content</div>双引号包围着
如果是单引号或双引号引起来的,那就简单了,只要编码单引号或双引号为HH;的形式即可,否则要编码的就多了:空格(包含ASCII十进制数的9、10、11、12、13、32)<>等。所以强烈建议用引号引起来。
使用ESAPI接口如下:
String safe = ESAPI.encoder().encodeForHTMLAttribute( request.getParameter( "input" ) );
规则三:进入JavaScript中时
其实这个有些复杂,修补的本质参考6.2节的内容,在此不再赘述,不过有一点要注意,如果是进入<script></script>里,小心输入的</script>闭合之前的<script>,无论这个</script>在JavaScript的什么位置。
使用ESAPI接口如下(不完备,大家多测试):
String safe = ESAPI.encoder().encodeForJavaScript( request.getParameter( "input" ) );
规则四:进入CSS中时
CSS非常松散,如果是定义具体某CSS属性的值,比如width的值,避免出现'"、;、}、{、(、)等特殊字符。
如果允许用户完整自定义CSS,则需要过滤掉javascript伪协议、expression及各种变形、@im-port及各种变形等,这些变形在<style>里或单独的CSS文件里往往是嵌入了\字符以及字母替换为\HH(十六进制),如果是在HTML标签style属性里,往往是字母替换为DD(十进制)或HH;(十六进制)形式。expression在IE 6下的任意字符替换为对应的全角也有效。
使用ESAPI接口如下:
String safe = ESAPI.encoder().encodeForCSS( request.getParameter( "input" ) );
规则五:进入URL时
一般就是对特殊字符采用%HH(十六进制)形式的编码,使用ESAPI接口如下:
String safe = ESAPI.encoder().encodeForURL( request.getParameter( "input" ) );
需要特别注意的是,如果输出的值是href/src等属性的完整值或前部分值,需要注意javascript:/data:等这类伪协议,一般采用白名单前缀协议进行限制会比较好,比如,只允许http://或https://打头的URL。
最后,这些防御方案的实施需要依据自己的场景进行,有的场景是不允许出现任何富文本的,此时直接在输出时进行HTML编码即可,而有的场景是允许部分富文本的(比如邮件系统、Blog系统),此时就不能简单地进行HTML编码了事,而是过滤掉那些危险的标签、事件等,但是我们通过前面的内容知道,这样过滤不会那么简单,HTML很松散,字符集编码有的时候是一个大问题,浏览器可能还有解析BUG,总是会出现相关的绕过。还会有更复杂的场景,我们只要清楚地知道用户输入到最终的输出整个过程的每个环节,保证在最终输出是安全的,那么就可防御了。
10.2.7 CSRF防御方案
CSRF的防御其实非常简单,为什么现在还有很多网站存在CSRF漏洞呢?因为这种攻击相对来说很少被重视。另外,代码参差不齐,总存在防御缺口的地方。针对CSRF攻击的防御,目前常用的有以下几种策略。
1. 检查HTTP Referer字段是否同域
一般情况下,用户提交站内请求,Referer中的来源应该是站内地址。如果发现Referer中的地址异常,就可以怀疑遭到了CSRF的攻击。这样,检查Referer字段是一个不错的方法,在浏览器客户端层面上,使用JavaScript和ActionScript已经无法修改HTTP Referer(除非0day)了。
这一招既简洁,效果又非常好,不过对来自站内的CSRF攻击就无能为力了。
2. 限制Session Cookie的生命周期
比如,用户登录网上银行,如果闲置10分钟,则自动销毁Cookie。这样的设置也可以在一定程度上减少被CSRF攻击的概率。
3. 使用验证码
虽然验证码的方式在一般情况下会降低用户体验的感受。但特定情况下,使用验证码方式是阻断CSRF攻击的有效手段。比如,网银中的付款交易需要用到验证码才能通过。
4. 使用一次性token
这是当前CSRF攻击中最流行的解决方案,to-ken是一段字母数字随机值,长度为10~40个字符,每次生成的值都是唯一的,所以往往会加上这几个因子:时间搓、用户ID、随机串等。
token经常出现在表单项中,比如,一个name是csrftoken的隐藏表单项,会随着表单提交而提交,然后服务端会校验该token值的合法性。校验的原理就是:在服务端生成这个唯一的token时,会保留下来,然后判断用户提交的表单是否有这个唯一的token。
这样,攻击者要得到这个token就比较难,CSRF攻击也就无法成功。这样的解决方案比验证码的用户体验好很多,但是仍需要如下几种方式才能得到这样的token:
· 这个token唯一性不够好,能够被猜测到,如仅根据时间搓+用户ID md5后的结果,这样的token就可能被预测到。
· 通过Flash crossdomain.xml跨域读取方式导致token泄漏,这个方式在前面已介绍过。
· 如果这个token还出现在GET请求中,那么很可能会因为Referer而泄漏。
除了上面这几种常规的方式,xeye的monyer还提了一种防御思路,在此整理出来供大家参考。
我们知道,一般防御CSRF有三种方法:判断Referer、验证码、token。
对于判断Referer来说,虽然客户端带用户状态的跨域提交,JavaScript和ActionScript已经无法伪造Referer了,但对于客户端软件和Flash的提交,一般是不带Referer的,那么就需要为此开绿灯,但这样使得外站的Flash请求伪造无法被防御。
而验证码的弊端很明显:会对用户造成影响。
token存在的问题是:时效性无法保证;大型服务时,需要一台token生成及校验服务器;需要更改所有表单添加的字段。
最近我们在做此类防御时,想出了另外一种方法,与xeye、woyigui等人在群里讨论后认为是可行的。其实原理非常简单,与token也差不多:当表单提交时,用JavaScript在本域添加一个临时的Cookie字段,并将过期时间设为1秒之后再提交,服务端校验有这个字段即放行,没有则认为是CSRF攻击。
token防CSRF的原理是:无法通过AJAX等方式获得外域页面中的token值,XMLHttpRe-quest需要遵守浏览器同源策略;而临时Cookie的原理是:Cookie只能在父域和子域之间设置,也遵守同源策略。
下面看一个简单的demo:
http://127.0.0.1/test.html: <script> function doit(){ var expires = new Date((new ate()).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为浏览器端的表单,其中有三个按钮:第一个是正常的表单提交;第二个是添加临时Cookie后提交表单;第三个是以hook submit事件来添加临时Cookie并提交。
正常的表单提交不会出现临时Cookie字段,第二个和第三个按钮提交则会出现。大家可以反复单击按钮来查看结果,但需要注意时间间隔需超过1 秒。(当然可以将test.html拿到外域看看,不过要注意form的target不能指向iframe了,可以以新窗口打开。由于同源策略,Cookie肯定是带不过去的。)
不过这种方式只适用于单域名站点,或者安全需求不需要“当子域发生XSS隔离父域”。因为子域是可以操作父域的Cookie的,所以它的缺陷也比较明显:这种方法无法防御由于其他子域产生的XSS所进行的表单伪造提交。而一个区分子域的自校验token是可以防止从其他子域到本域的提交的。但如果对于单域而言,这种方法应该是足够的,并且安全性可能会略大于token。
这种方式没有什么大问题,不过确实有一些小的疑问,比如:
· 网络不流畅,有延迟会不会导致Cookie失效。这个显然是不会的,因为服务端Cookie是在提交请求的header中获得的。延时在服务端,不在客户端,而1秒钟足可以完成整个POST表单的过程。
· Cookie的生成依赖于JavaScript,相当于这个token是明文的?这是肯定的,不管采取多少种加密,只要在客户端,就会被破解,不过不管怎样,CSRF无法在有用户状态的情况下添加这个临时Cookie字段。虽然服务端curl等可以,但是无法将当前用户的状态也带过去。
· 外站是否可以伪造这个临时Cookie呢?目前看来至少通过ActionScript和JavaScript无法向其他域添加和更改Cookie,通过服务端虽然可以伪造Cookie,但不能获取到目标域的用户状态。
· 如果由于某种网络问题无法获取Cookie呢?那么用户状态也不能获取,用户只能再提交一次才可以。
说实话,我们无法确定这种新方法究竟是否真正有效,也许存在被绕过的可能性。如果真有效,那么这大概是一种最简单的、对代码改动最小,且对服务器压力也最小的防御CSRF的方法。
10.2.8 界面操作劫持防御
在第5章介绍过界面操作劫持,它的防御方法更简单。基于界面操作劫持的攻击模式是用巧妙的视觉欺骗的方式,对Web会话进行劫持。防御这种攻击的技术思路是:使有重要会话的交互页面不允许被iframe嵌入,或者只允许被同域iframe嵌入。
目前针对界面操作劫持的防御有以下几种。
1. X-Frame-Options防御
X-Frame-Options是由微软提出来的防御界面操作劫持的一种方法,Web开发人员可以在HTTP响应头中加入一个X-Frame-Options头,浏览器会根据X-Frame-Options字段中的参数来判断页面是否可以被iframe嵌入,如图10-1所示。
图10-1 X-Frame-Options头
X-Frame-Options有两个参数:DENY和SAMEORIGIN。如果是参数DENY,下面这个Frame_deny.php页面就不能被嵌入到<iframe>中。
<?php header ("X-Frame-Options: DENY"); ?> <b>Deny_test</b><br>
如果是参数SAMEORIGIN,那么下面这个Frame_same.php页面只能被嵌入在和其同源的页面中。
<?php header ("X-Frame-Options: SAMEORIGIN"); ?> <b>Same_test</b><br>
目前支持X-Frame-Options的浏览器如表10-2所示。
表10-2 支持X-Frame-Options的浏览器
浏览器 | 支持版本 |
IE | 8.0以上 |
Opera | 10.0以上 |
Safari | 4.0以上 |
Chrome | 5.0以上 |
Firefox | 3.6.9以上 |
以上面的Deny程序为例,设置Frame_test.php 内容如下:
<b>X-Frame-Options_test</b><br> <iframe src=frame_deny.php width=500height=300></iframe>
比如,IE 9下的防御提示如图10-2所示。
图10-2 IE 9浏览器防御界面操作劫持
2. Frame Busting脚本防御
防御操作劫持的另一种方法是使用JavaScript脚本来对页面进行控制,达到页面无法被iframe嵌入的目的,这样的防护脚本被称为Frame Busting脚本。下面给出一段Frame Busting脚本示例。
<script> if (top.location != self.location) { top.location=self.location; } </script>
代码中的top指代主体窗口对象,self指代当前窗口对象。如果判断出页面的主体窗口地址与当前窗口地址不同,就将主体窗口地址设置为当前窗口地址。这样就避免了用户的操作实际发生窗口与所见窗口不一致的情况,从而防护了利用透明层进行操作劫持的攻击方法。
但是,以上这段Frame Busting代码也存在一些问题:在IE浏览器下,如果我们在主体窗口中加入<script> var location="" </script>这段代码,那么这段Frame Busting代码就会被突破。
X-Frame-Options和Frame Busting方法都可以做到对界面操作劫持的防御。相对而言,X-Frame-Options的方式还是比Frame Busting更安全。X-Frame-Options是在浏览器中嵌入的,而Frame Busting是脚本控制。这就意味着JavaScript代码始终有被突破的可能性。
这种突破Frame Busting的方法称为BustingFrame Busting。Collin Jackson在2010年的Bust-ing Frame Busting:a Study of Clickjacking Vulner-abilities on Popular Sites报告中,对Frame Bust-ing脆弱性做了深入研究,讨论了10类FrameBusting被突破的情况,也列举了包括Yahoo、Facebook等知名网站Frame Busting被突破的真实案例。
所以在X-Frame-Options机制没有普及的情况下(例如,使用IE 6版本的浏览器的用户还有一些),我们大部分时间还是要依赖于使用JavaScript脚本来抵御界面操作劫持的,所以严格编写合格的Frame Busting代码,才能使防护更加安全。下面给出一段目前防御性最高的Frame Busting代码,这段代码由Collin Jackson的报告中给出:
<style> html {display:none;} </style> <script> if(self==top){ document.documentElement.style.display='block'; } else{ top.location=self.location; } </script>
3. 使用token进行防御
在业界主流的防御界面操作劫持攻击的方法中,似乎并没有提及防御CSRF中的token也可以对其进行防御。
比如,这样一个登录页面:http://url/login?idtoken=90dfOlk,如果攻击者无法猜测到idtoken后面的数值,那么这个登录页面就不能被iframe嵌入,进而也无法对这个页面实施界面操作劫持攻击。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论