DOM-Based XSS是一种基于文档对象模型(Document Object Model,DOM)的Web前端漏洞,简单来说就是JavaScript代码缺陷造成的漏洞。与普通XSS不同的是,DOM XSS是在浏览器的解析中改变页面DOM树,且恶意代码并不在返回页面源码中回显,这使我们无法通过特征匹配来检测DOM XSS,给自动化漏洞检测带来了挑战。在2012年,腾讯安全平台部就研发并使用了基于QTWebkit引擎的检测方案。
然而,由于Web前端语言灵活多变,各项业务代码复杂不一,加上部分前端开发人员安全意识不足,仍源源不断有存在JavaScript缺陷的页面产生,使得DOM XSS的检测出现了不少“漏网之鱼”。更加雪上加霜的是,由于JavaScript是一门客户端脚本语言,其代码逻辑可以被任意用户查看到,所以不少“治标不治本”的DOM XSS对抗策略被攻击者再次绕过,刚爬出一个坑,又跌落另一个坑。
DOM XSS就像行人在广场随地乱吐的口香糖,成了Web前端挥之不去的安全梦魇。被随手编写出来容易,查找和修复却反反复复,费时费力。无论是目前安全平台部研发的门神系统,还是DOM XSS安全JS,都只能称作缓兵之计。若能在开发过程中,有针对性的留意容易造成DOM XSS的JavaScript代码多留一个心眼,对传入的数据做严格的过滤,将其限制在可控范围内,才有可能从根本上解决DOM XSS。毕竟所有漏洞的终极良药就是安全的开发意识和规范。驱散前端安全梦魇,保护用户安全,需要从开发到运维的多方面共同努力。
本文将首先讲解DOM XSS不可小觑的危害,接着总结高发DOM XSS的业务场景,给出缺陷代码实例,并详述漏洞产生原因,最后分类给出有效彻底的防护过滤手段。
比漏洞本身更具危害的是:掉以轻心。
一般情况下,XSS给人最深的印象似乎就是弹不完的浏览器提示框,除了恼人外,似乎无关痛痒:
但就算是小小的一个反射型DOM XSS,如果落到黑产手中,也会被利用的“淋漓尽致”,让成千上万的用户带来损失。以邮箱业务下的DOM XSS为例,通过该漏洞,黑产能够偷取到邮箱用户的cookie。借助这个“令牌”,攻击者就能够自由出入用户的邮箱,收发用户的私密邮件。至此,一方面攻击者能够劫持中招用户继续扩散恶意邮件,偷取更多邮箱用户的“令牌”,进而控制更多的邮箱;另一方面,黑产可以获取用户邮箱中的敏感信息,进而重置邮箱用户在其他网站注册的账户密码。试想,在如今生活方方面面离不开互联网的今天,如果一位用户将邮箱账户同时绑定了Apple iCloud账户,网盘账户,金融支付账户,邮箱的沦陷将带来多大的“雪崩式”效应。
你以为这就是全部了?如果DOM XSS如果出现在了客户端产品的“特权域”,危害将会被进一步升级。首先向大家来解释一下什么是“特权域”。特权域是部分客户端产品,因为要实现WEB和客户端产品的交互,比如在网页内点击链接自动安装一个浏览器插件,自动向手机推送一条消息。但同时出于安全考虑,又不能让所有WEB域都调用这些具有“特权”的接口,因此基于“白名单”思想,客户端开发工程师设下了“结界”,限制了类似只有如“*.qq.com”域名下将能够调用“结界”内的API接口。因为一般来讲,借助“白名单”思路,API的调用是十分可控的。但如果这些特权域下出现了DOM-XSS,攻击者可以通过引入外部恶意JS,让自己瞬间具备了调用特权API的权限,至此,工程师设下的“结界”被捅破严重的情况下,甚至可以导致客户端产品的远程代码执行。
一、 在前端实现页面跳转
在很多场景下,业务需要实现页面跳转,常见的实现方式一般有三种,第一种是设在后端设置302跳转Header或通过函数接收参数实现跳转,第二种是使用Meta标签实现跳转,最后一种是通过JavaScript实现跳转。不少Web前端工程师对最后一种跳转的方式情有独钟,最常用到的方法有: location.href / location.replace() / location.assign()。
也许提到页面跳转业务场景下的安全问题,你首先会想到限制不严导致任意URL跳转,而DOM XSS与此似乎“八竿子打不着”。但有一种神奇的东西叫“伪协议”,比如:“javascript:”、“vbscript:”、“data:”、“tencent:”、“mobileqqapi:”等,其中“javascript:”、“vbscript:”、“data:”在浏览器下可以执行脚本:
(图:使用“javascript:”伪协议在页面内执行脚本)
最为要命的是,使用这些伪协议执行的JavaScript代码的上下文(context)就是当前页面,也就相当于在页面内注入了一段恶意JavaScript代码。至此,攻击者也就能实施0x01中提到的攻击了。
经过前几年DOM XSS狂轰滥炸式的洗礼,Web前端工程处理起相关跳转代码逻辑来,也个个都是有经验的老司机了。直接从各种来源取跳转目标URL,然后通过上面提到的三个JavaScript实现跳转的方式,已销声匿迹。
你以为这样就结束了?这个世界上,还有一种比老司机更厉害的生物,那就是“老老司机”——时刻虎视眈眈查找业务漏洞的攻击者们。之前提到,由于JavaScript是一种客户端脚本语言,如果说Web后端代码有一层“窗户纸”护着的话,那位于Web前端的JavaScript代码就时时刻刻“天窗大开”和访客“坦诚相见”。所以,以下若干种过滤对抗手段,一下子就会眼尖的攻击者绕过,并继续大摇大摆的构造攻击:
常见缺陷1:使用indexOf判断URL参数是否合法
示例缺陷代码:
JavaScript相关规范中指出,indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。该方法将从头到尾地检索字符串 stringObject,看它是否含有子串 searchvalue。
也就是说,如果传入的URL中带有indexOf的关键词,那indexOf方法将直接返回true。拿上面的缺陷代码为例,只要攻击传入的URL中带有tmast://,indexOf将直接返回true,并进入跳转逻辑。所以攻击者只要构造”javascript:alert(1);//tmast://”即可完成攻击,又因为“tmast://”位于JavaScript代码的注释部分,所以JavaScript代码运行时会直接忽略,但indexOf却认为URL中存在“tmast://”直接放行。
常见缺陷2:正则表达式缺陷
示例缺陷代码 [1]:
示例缺陷代码 [2]:
聪明的Web前端工程狮,当然知道indexOf下潜藏的“深坑”,所以祭出了神器“正则表达式”。但不曾想“阴沟翻船”,有对URL进行了严格限制的意识,比如跳转页面只能是qq.com/paipai.com,认为这样就可以解决DOM-XSS和URL跳转的问题,但忘了一个神奇的符号“^”,加上和不加上,过滤的效果具有天壤之别。因为正则没有严格限制传入的URL开头只能是“http”或“https”,攻击者仍然可以构造“javascript:alert(1);//http://www.qq.com”来绕过看似严格的过滤。
修复技巧
在前端实现页面跳转业务场景下,正确的过滤实现方法是,严格限制跳转范围。一方面要限制能够跳转页面的协议:只能是http、https或是其他指可控协议;另一方面,要严格限制跳转的范围,如果业务只要跳转到指定的几个页面,可以直接从数组中取值判断是否这几个页面,如果跳转范围稍大,正确使用正则表达式将跳转URL严格限制到可控范围内。
二、 取值写入页面或动态执行
除接收URL参数经后端处理最后在前端展示外,在Web前端通过JavaScript获取不同来源的参数值,不经后端处理即刻在Web页面进行展示或动态执行的业务场景也十分常见,虽然通过此方法,优化了用户的浏览体验,但也带来了满满的安全风险。
想要在客户端实现接受参数并写入页面或动态执行,就不得不用到JavaScript“三姐妹”,她们分别是:innerHTML、document.write、eval。“三姐妹”具有强大的功能的同时,不经意间也成了DOM-XSS攻击的导火索。因为JavaScript取值的来源纷繁复杂,如:Parameter、Cookies、Referer、Window name、SessionStorage等,工程师稍有不慎忘记做转义处理,或过分相信取值来源的数据,直接将分离出的参数值交给JavaScript“三姐妹”处理,就有可能招来DOM-XSS。接下来,将按不同数据源,详述每种业务场景下易造成DOM-XSS的代码缺陷。
常见缺陷1:从URL中的取参数值写入页面或动态执行
示例缺陷代码[1]:
粗心的工程狮直接从URL的锚参数(即位于#后面的参数)中取值,不经过任何处理直接innerHTML写入页面,导致攻击者只需要构造如下URL即可完成一次DOM XSS攻击:
由于整个攻击过程在客户端侧完成,不需要向服务器发送任何请求数据,所以即便业务接入了对抗反射型XSS的Web应用防火墙(WAF),这类DOM XSS也无法被感知,攻击者便可畅通无阻的利用漏洞对用户开展攻击。
示例缺陷代码[2]:
当然,不只是innerHTML一种方法,只要传入的参数值没有做任何处理,并进入到JavaScript“三姐妹”类似的函数中,就会产生DOM XSS漏洞。就比如在此案例下,页面内引入了jQuery库,JavaScript的initUI函数直接将获取到的未经过滤的name参数,通过“$().html()”的方式写入了页面,进而可以被攻击者利用,进行基于DOM XSS漏洞的攻击。
常见缺陷2:从Cookie中的取参数值写入页面或动态执行
示例缺陷代码[1]:
示例缺陷代码 [2]:
依据相关规范,在浏览器中不同域下的Cookie有隔离措施,即在google.com下是不能向qq.com下设置cookie的,可以说cookie这个来源相对来说较为可靠。但万事总是不是绝对的,由于过分相信cookie这个来源,除了直接从cookie中取值作为判断用户身份的依据造成任意用户账户登录的的高危逻辑缺陷外,不安全的cookie操作方式也产生了大量的DOM-XSS。
示例缺陷代码[1],直接从cookie中取值写入页面或动态执行,原理基本同从URL中的取参数值写入页面或动态执行,只是换了一个取值来源而已,相信各位已经有了大概了解。但同时我们注意到,还有一种较为特殊的业务场景,示例缺陷代码[2]:取cookie键值,动态拼接要页面引入前端资源的URL。在此场景下,工程师已经对HTML常见的特殊字符做了过滤,是不是就安全了呢?并不。一般情况下,进行转义的HTML的特殊字符如下:
在上面这个案例中,如果window.isp取到的值为“www.attacker.com/”,最终拼接出来的静态资源URL路径为:http://www.attacker.com/victim.com,因为“.”和“/”都不在转义范围内,导致攻击者可以向页面引入自己站点下的恶意js文件,进而实施DOM-XSS攻击。
常见缺陷3:从localStorage、Referer、Window name、SessionStorage中的取参数值写入页面或动态执行
示例缺陷代码:
从localStorage、Referer、Window name、SessionStorage数据源中取数据,也时常是栽跟头的高发地。上面这段示例代码中,就首先取window.name的值,最后直接innerHTML到页面中。一般情况下,页面的window.name攻击者不可控,故往往会被认为来源相对可信。但借助iframe的name属性,攻击者可以将页面的window.name设置为攻击代码,仍然可以
通过下面这段Payload,构造DOM XSS:
修复技巧
1. 写入页面前先转义。在取值写入页面或动态执行的业务场景下,在将各种来源获取到的参数值传入JavaScript“三姐妹”函数(innerHTML、document.write、eval)处理前,对传入数据中的HTML特殊字符进行转义处理能防止大部分DOM-XSS的产生。此外,根据不同业务的真实情况,还应使用正则表达式,针对传入的数据做更严格的过滤限制,才能保证万无一失。
2. 慎用危险的“eval”。需要强调的是,由于JavaScript中的eval函数十分灵活,能够支持执行的字符串编码纷繁复杂。强烈建议,不到万不得已,不要使用eval函数处理不可控的外部数据。
3. 编写安全的函数方法,从看似“可靠”的数据源获取参数值。无论是从cookie,还是从localStorage、Referer、Window name、SessionStorage中获取数据,都应使用安全的函数,对传入的数据做过滤后,再传递给相关函数写入页面或执行。
三、 使用HTML5 postMessage进行跨域通讯
示例缺陷代码:
HTML5引入的postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。其本意是方便Web前端开发者实现跨域通讯,由于浏览器遵循同源策略(Same Origin Policy),所以如下跨域向页面写入内容的操作将会被阻断:
但借助postMessage()方法,Web前端开发者就能实现跨域从site-a.net向site-b.net下页面写入内容。然而,开发者享受便利的同时,也却往往疏忽大意,忘记对postMessage的来源event.origin进行限制,导致任意域名下的页面只要通过如下代码,就可以与存在缺陷的页面进行跨域交互通讯,也再次落入DOM XSS的深坑:
修复技巧:
修复此类漏洞的方法简单直接,只要在页面进行innerHTML操作前做一次even.origin的判断。当然,在innerHTML前,将event.data的数据进行一次HTML特殊字符转义,将会锦上添花,确保万无一失。
四、使用存在缺陷的第三方库或通用组件
常见缺陷:jQuery低版本(1.9.0以下)存在DOM XSS漏洞可导致用户身份被盗用 (参考:http://bugs.jquery.com/ticket/9521)
(图:漏洞利用代码传入$()函数执行可触发漏洞)
jQuery 是一个非常流行的JavaScript 库,但低版本的jQeury存在设计缺陷,导致引入低版本的jQuery文件之后,若对用户传入的参数值没有进行处理即传入$()函数中执行,且参数值中存在html标签,即$(‘〈 img src=x onerror=alert()〉‘),jQuery会自动生成该html标签并加载在页面中,可导致DOM XSS漏洞。
0x03 DOM XSS通用解决方案一、参考/使用filter.js库
filter.js由一系列针对常见业务场景下造成DOM XSS的恶意数据开展过滤的函数组成。其中包括:VaildURL、HtmlEncode、HtmlAttributeEncode等函数方法。在使用过程中,针对上面一部分提到的不同的场景需要,使用与之对应的过滤函数进行验证,才可以从根本上防止DOM XSS产生。
(图: aq_common.js实现原理及防御效果)
举个例子,当业务需要在前端实现页面跳转时,根据之前提到的修复策略,就应使用VaildURL方法对跳转目标做一次过滤验证。
当然在使用filter.js之前,需要根据业务真实情况,对其中部分JavaScript函数做修改,比如上面这个校验跳转目标网址的函数,就应根据自身需要基于正则表达式做相关扩充或更严格的限制。
二、Web前端应用防护方案
门神DOM-XSS防御JS是腾讯安全平台部为解决XSS问题提出的一套Web前端应用防护方案,与运行在后端的门神WAF是“亲兄弟”。但相较于传统WAF,门神DOM XSS防御JS通过JavaScript代码实现,所以能在客户端持续生效,可以缓解DOM XSS漏洞带来的危害。
(图: 门神DOM-XSS防御JS实现原理及防御效果)
在参考/使用filter.js库的同时,也不妨参考门神DOM XSS防御JS,给站点多加一层“防护罩”。不过需要提示的是,类似门神DOM XSS防御JS这样的Web前端应用防护,仅能提高常见DOM XSS攻击的门槛,并不能从源头上修复并解决XSS。所以要根除DOM XSS还需要从存在缺陷的代码入手,对获取的参数值做严格过滤限制,才能真正治本。
三、定期进行漏洞检测
在业务上线前后,使用漏洞扫描系统,定期进行漏洞检测有助于及时发现和处理JavaScript代码中的缺陷。
腾讯云的客户可以使用自带的“云安全 QS”,很方便地进行定期漏洞检测。访问“https://console.qcloud.com/host/defect/exam”,在如下界面添加要扫描的站点即可:
(图: 腾讯云“主机与网站安全”网站漏洞检测系统)
用一句话来总结所有DOM XSS的场景,就是:不可控的危险数据,未经过滤被传入存在缺陷的JavaScript代码处理,最终触发DOM XSS漏洞。而在制定防护DOM XSS漏洞方案时,应首先树立起“安全的开发意识和规范”,从存在缺陷的代码入手,从源头解决DOM XSS,最后辅之以Web前端应用相关防护措施。
在防御和消灭DOM XSS漏洞的斗争一线,希望Web前端工程师们树立起安全的开发意识,与广大运维安全人员携手,将DOM XSS漏洞消灭在萌芽之中。驱散前端安全梦魇,保护用户安全。
0x05 参考资料1. [腾讯安全应急响应中心] 基于QtWebKit的DOM XSS检测技术
https://security.tencent.com/index.php/blog/msg/12
2. [Hewlett Packard] XSS and App Security through HTML5's PostMessage()
http://community.hpe.com/t5/Protect-Your-Assets/XSS-and-App-Security-through-HTML5-s-PostMessage/ba-p/6515002#.V7FTFk196Ul