跨站脚本攻击,英文全称为Cross Site Script,为与层叠样式表(Cascading Style Sheet,CSS)有所区别,缩写为XSS。XSS是指攻击者向网页中插入恶意代码,当用户浏览该页面时,嵌入在其中的代码会被执行,从而达到恶意攻击用户的目的。相较于SQL注入攻击,XSS攻击的对象是网站用户,SQL注入的攻击对象是网站服务器。XSS漏洞通常是通过PHP的输出函数将JavaScript代码输出到HTML页面中,通过用户本地浏览器执行的,所以XSS漏洞关键就是寻找参数未过滤的输出函数。一开始这种攻击的演示案例是跨域的,所以称其为跨站脚本,但是发展到如今,由于JavaScript的强大功能以及网站前端应用的复杂化,是否跨域已经不再是重点。因为历史缘故,XSS这个名字一直保留了下来。
XSS攻击手段和目的
- 盗用cookie,获取敏感信息
- 利用植入Flash,通过crossdomain权限进一步获取更高权限;或者利用Java得到等类似的操作,即提权
- 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以被攻击者的身份执行一些管理动作,或执行一些一般操作如发微博、加好友、发私信等操作。
- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如不当的投票活动等
- 在访问量极大的页面上的XSS可以攻击一些小型网站,实现DDOS攻击的效果。
分类
反射型XSS
攻击者事先制作好攻击链接,用户自己去点击链接时,服务器端接受数据后处理,把带有XSS代码的数据发送到浏览器,浏览器解析这段带有XSS代码的数据后,触发XSS代码。一般容易出现在搜索页面,是一次性攻击,又称为“非持久性XSS”。
存储型XSS
存储型XSS。允许用户存储数据的Web应用程序都肯出现存储型XSS漏洞,当攻击者在某网页提交一段XSS代码后,服务器端会接收并存储。当有用户访问该页面时,这段XSS代码被程序读出来相应给浏览器,造成XSS跨站攻击。这种XSS有很强的稳定性,隐蔽性也比较强。这种XSS攻击多出现在个人信息或发表文章的地方、留言板等。攻击者在这类地方写下代码,如果网页没有过滤或过滤不严,每当有用户访问该页面的时候都会出发代码执行,这种XSS非常危险,容易造成蠕虫,大量盗窃cookie。又称为“持久性XSS“。
DOM型XSS
DOM型XSS从效果上来看也是属于反射型XSS,但是被单独划了出来,因为它的成因较为特别。DOM型XSS就是通过修改页面的DOM节点形成的XSS。
文档对象模型(Document Object Model),DOM是一与平台、编程语言无关的接口,它允许程序或脚本动态地访问和更新文档内容、结构和样式,处理后的结果能够成为显示页面的一部分。DOM通常用于代表在HTML、XHTML和XML中的对象。使用DOM可以允许程序和脚本动态的访问和更新文档的内容、结构和样式。
通过JavaScript可以重构整个HTML页面,而要重构页面或者页面中的某个对象,JavaScript就需要知道HTML文档中所有元素的“位置“。而DOM为文档提供了结构化表示,并定义了如何通过脚本来访问文档结构。根据DOM规定,HTML文档中的每个成分都是一个节点。
DOM的规定如下:
- 整个文档是一个文档节点;
- 每个HTML标签是一个元素节点;
- 包含在HTML元素中的文本是文本节点;
- 每一个HTML属性是一个属性节点;
- 节点与节点之间都有等级关系。
HTML的标签都是一个个节点,这些节点组成了DOM的整体结构:节点树↓
可以发现,DOM本身就代表文档的意思,而基于DOM型的XSS是不需要与服务器端交互的,它只发生在客户端处理数据阶段。
当我们访问网页时,会向网页服务器发送请求,服务器收到请求会进行DOM分发,依次将主页、css、js、媒体文件等发过来(我们常能注意到网速慢时网页的主页或文字内容会先于图片、音视频内容加载出来),然后在本地浏览器上执行DOM树(按照顺序加载)。
DOM型XSS即是在DOM分发后加载执行来的。所以它不依赖提交数据到服务器端,而是从客户端获得DOM中的数据在本地执行。
XSS payload
XSS攻击成功后,攻击者能够对用户当前浏览的页面植入恶意脚本,通过恶意脚本,控制用户的浏览器。这些用以完成各种具体功能的恶意脚本,被称为“XSS payload“。这个payload可以翻译为载荷。XSS payload实际上就是JavaScript脚本,还可以是Flash或其他富客户端
(Rich Client,胖客户端【富客户端】,与瘦客户端相对。富客户端和瘦客户端的区别可见blog.youkuaiyun.com/csdnlijingran/article/details/100162691)的脚本。所以任何JavaScript脚本能实现的功能,XSS payload都能做到。正如上文提到过的,XSS的常见作用就是读取浏览器的Cookie,发起Cookie劫持攻击。
XSS的防御:
HttpOnly
HttpOnly最早由微软提出,并在IE 6 中实现的,至今已经成为一个标准。浏览器将禁止页面的JavaScript访问带有HttpOnly属性的Cookie。在我之前的HTTP与Cookie笔记(blog.youkuaiyun.com/Yisitelz/article/details/131954649)中的cookie与session一节提到过,Set-Cookie消息头就包含这个属性。
如果该Cookie设置了HTTP Only,那么XSS偷Cookie的攻击就会失败,因为JavaScript不允许读取Cookie的值。另外,服务器上可能会设置多个Cookie,HttpOnly可以选择性的加在其中的任何一个上。有选择地将HttpOnly属性加给用于认证功能的关键Cookie。
输入检查
常见的web漏洞如XSS、SQL注入等,都会要求攻击者构造一些特殊字符,而这些特殊字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。输入检查的逻辑必须放在服务器端代码中实现。
有些JavaScript能对用户的输入进行验证处理,但其归根结底是属于前端验证,在浏览器未提交数据时进行的验证,而我们可以用正常的输入通过验证,然后在拦截HTTP请求后修改数据。此时看来JavaScript的验证根本起不了任何作用。由此可见前端验证是不可靠的。前端JavaScript验证是为了防止用户输入错误,服务器端验证是为了防止恶意攻击。
在这方面的防御上,键入检查一般是检查用户输入的数据中是否包含了一些特殊字符,比如
< > ‘ “ 等,如果发现特殊字符,则将这些字符过滤或编码。
一些较为智能的键入检查还可以匹配XSS的特征,比如查找用户数据中是否包含了<script>、JavaScript 等字样的敏感字符。
这种XSS防御方式可以称为XSS Filter,XSS Filter在用户提交数据时获取变量,并进行XSS检查,但此时用户数据并没有结合渲染页面的HTML代码,因此XSS Filter对语境的理解并不完整。比如下面这个XSS漏洞
其中$var是用户可以控制的变量,这时候用户只需要在表单中提交一个恶意脚本所在的URL地址,就能直接实施XSS攻击。而对于一些XSS Filter来说,无法看到用户数据的输出语境,只能看到用户提交的URL,这就很容易产生漏报,因为多数情况下URL是合法的用户数据。XSS Filter对 < > 等字符的处理还有可能改变数据语义,本来 x>2 或y<1 取值范围表示的好好的,被粗暴的过滤了,未免贻笑大方。
还有,如果用户的昵称中包含一些特殊字符(比如早年的杀马特非主流昵称),因为其用途或性质,可能会在不同的情境中多次出现,在各个地方的语境也不一定相同,展示的需求也不相同,如果单一地使用替换、过滤操作,也是会影响用户体验。
输出检查
一般来说,除了富文本的输出外,在变量输出到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。
安全的编码函数
编码分为很多种,针对HTML代码的编码方式是HtmlEncode,HtmlEncode并非专用名词,它只是一种函数实现。它的作用是将字符转化成HTMLEntities。
为了对抗XSS,在HtmlEncode中要求至少转换以下字符。
- & 转义成 &
- < 转义成 <
- > 转义成 >
- “ 转义成 "
- ‘ 转义成 ' ;&apos;不推荐用后面的,因为它不在HTML4entity的官方列表上
- / 转义成 /; 包含反斜线是因为他可能会闭合一些HTML entity。
在PHP里,有htmlentities()和htmlspecialchars()两个函数可以满足安全要求。
相应地,JavaScript的编码方式可以使用JavascriptEncode。
JavascriptEncode和HtmlEncode的编码方法不然,它需要用 “ \ ” 对特殊字符进行转义。在对抗XSS时,还要求输出的变量必须要在引号内部,以避免造成安全问题。比如下面两种写法:
如果escapeJavascript()函数只是转义了几个危险字符,比如 “ ‘ < > \ 等,那么上面两行的代码输出后有可能会变成
我们可以发现第一行会执行额外的代码,而第二行则是安全的,对于第二行的,攻击者若想逃出引号的范围,也会比较困难
所以要求使用JavascriptEncode的变量输出一定要在引号内。如果开发者没有这样的习惯,就只能用更严格的JavascriptEncode函数来保证安全,把除了数字、字母外的所有字符都采用十六进制 “ \xHH “ 的方式进行编码。
只用编码还不够
XSS攻击主要发生在MVC架构中的View层。大部分的XSS漏洞都可以在目标系统中解决。在Python的开发框架Django自带的模板系统“Django Templates“中,可以使用escape 进行HtmlEncode。比如 {{ var| escape }} 这样写的变量会被HtmlEncode编码。
这一特性在Django 1.0中得到了加强,默认所有的变量都会被escape,这个做法符合之前的那篇文章中提到的“Secure By Default“原则。
在Python的另一个框架web2py中,也默认escape了所有的变量。Django和web2py都选择了在view层默认HtmlEncode所有变量以对抗XSS,出发点是好的,但是这不能解决所有的XSS问题。前面的文章安全方案设计中提到过,要在正确的地方使用正确的编码方式
比如这个例子
结果就是浏览器弹了两次窗。导致XSS攻击发生的原因就是没有分清楚输出变量的语境。XSS的防御需要分情况对待。
正确地防御XSS
见我的另一篇笔记blog.youkuaiyun.com/Yisitelz/article/details/132060413
参考资料:《Web安全深度剖析》张炳帅
《白帽子讲web安全》吴瀚清