DOM-based XSS 与存储性 XSS、反射型 XSS 有什么区别?

关注者
515
被浏览
116,163

13 个回答

XSS就是XSS。所谓“存储型”、“反射型”都是从黑客利用的角度区分的。对于程序员来说意义不大,反而是误导。只有“DOM-based”型略有不同。

XSS、SQL injection之类的漏洞,原理都是破坏跨层协议的数据/指令的构造。

如SQL注入,涉及应用层和数据库层。协议是SQL查询语言。对于应用层来说,一句sql是数据(字符串);对于数据库层来说,一句sql是指令。sql注入的原理,就是破坏sql的构造。防御的方法,就是用参数查询(现代所有的数据库驱动的api一定包含了)而不是自己拼sql字符串。这个已经是所有后端工程师的基本常识了。

而XSS,两个层次是服务器端和浏览器端。协议就是HTML/CSS/JavaScript。对于服务器端来说,html是数据(字符串);对于浏览器端来说,html是指令。XSS的原理,就是破坏html/css/js的构造。

防御的方法,一般认为是正确escape,就是替换尖括号、引号等特殊符号。

但是这是不够的,因为这只解决了html的问题。考虑如下:

<script>var name = '<?= $name ?>';</script>

这代码显然有XSS隐患。

那么我们escape一下,是不是就好了?

<script>var name = '<?= htmlspecialchar($name) ?>';</script>

很遗憾这样是没用的。因为这里是javascript输出点,xss破坏的目标是破坏js构造而不是html构造。html构造中的关键字符是尖括号、双引号、“&”符号等。而js构造就复杂了,比如换行、注释(//和/*)、引号(包括单引号)等都会改变构造。

为了确保js构造的正确,应该:

<script>var name = <?= json_encode($name) ?>;</script>

不过这还是存在一个漏洞。(作为一个简单的习题留给同志们。)

【另外PHP不同版本的json_encode的行为不一致,几乎都有问题,虽然不至于直接XSS,但存在被利用的可能。】

显然,PHP没有提供比较便利的方式来确保代码的安全,这是那个年代(199x)服务器端脚本技术的通病。遗憾的是,web技术发展到今天,即使是常见的现代Web模板,大多提供了默认的html escape,但对inline script中的XSS防御就乏善可陈了。

我开发的 jedi 模板(

baixing/jedi · GitHub

)在这点上做了较好的创新:

1. 不允许在 script 元素中直接进行内容插值。

2. 如果要给 inline script 传递数据,写成:

script = $name
  !
    console.log(data)

会被编译为:

<script>
void function(data){
  console.log(data)
}(<?= safe_json_encode($name) ?>)
</script>

可以看到,我用的这种方式,和sql的参数查询是类似的,就是提供更好的抽象——API/模板指令。

回到题主的问题。所谓“存储型”指的是$name的值来自于持久存储,比如数据库。“反射型”指的是$name的值来自于直接的用户输入(通常通过URL参数传递)。

黑客更喜欢“存储型”,或者说“存储型”的“危害”更大的原因是,$name可能会被用在一个页面的很多地方,或者很多个页面中,甚至多个站点中。所有这些地方很可能或者必然不是由单个程序员写的,可能是几个月之前,或者几年之后写的。黑客会扫描所有出现的地方,只要其中有一处没有正确输出,就是一个XSS漏洞点。大多数码农(以及他们的manager)在面对漏洞的时候,只是头痛医头脚痛医脚。而“存储型”因为有太多可能的点,所以常给人以防不胜防,堵不胜堵的感觉。

对于“存储型”,有一种看似可行的防御方法是,在存入数据库时预先escape,这样只要在存入这个单点保证代码正确,而不用管在哪里输出。看上去是一劳永逸,但是实际上是糟糕的应对方法。

为什么?

翻看答案前请开动脑筋想一下。











提示:前面的例子已经包含了原因。












答案:此方式实际上限制了所有输出点都是html,但绝大多数情况下,你无法也不应预设所有输出的点必须是html。常见的情况是,有些地方需要html输出,有些地方需要js输出,有些地方可能是纯文本输出(比如手机客户端)。

html输出和js输出需要做的escape是不同的,如果是纯文本输出,你根本不需要escape。如果你同时做多种escape,你就要正确的unescape,显然就丧失了不用管输出点的“优点”。


最后说一下 DOM-based XSS。直接来一个反面教材吧。

百度翻译不久前爆了一个xss漏洞。百度翻译除了给出翻译结果之外,如果这个词有百度百科词条的话,也会通过ajax拉百度百科的内容并显示在页面上。问题他们是直接把得到的百度百科内容通过innerHTML插入页面,并且没有做escape,这导致百度百科的内容中的html标签(比如那些html、css、js相关的词条上的代码sample)直接就生效了。可想而知,你只要修改一下百度百科上的词条就能轻松让对应的百度翻译页面带有XSS漏洞……

“DOM-based”确实有一点独特性。那就是涉及的层次不同。所谓基于DOM的XSS,涉及的两个层次不是服务器端和浏览器端,而是浏览器端的JavaScript层和HTML层。更准确的说,就是服务器脚本变成了客户端脚本。

因为改写innerHTML在传统前端开发中非常常见,这情形就好像当年服务器端脚本直接拼html一样(我的第一个php的例子)。另一方面,如果存在eval或者new Function调用,前述的针对js构造的XSS攻击也是可能的。

那么对于所谓 DOM-based XSS 的正确的防御办法是什么呢?













比照一下就知道了,一个就是一样的正确escape。另一个自然就是用更好的抽象——在这里就是DOM API了。比如通过document.createTextNode(someText)创建文本节点后插入,自然不可能有XSS的可能性。不过之所以我们都用innerHTML,不就是因为DOM API用起来不顺手吗?(慢倒还在其次,因为现代浏览器的脚本引擎都优化了,以至于不再慢甚至反超innerHTML了。)这个时候新的Template元素就是大杀器了——详细这里不再展开,请自行学习体会。当然如果你要考虑兼容性,那么用一款靠谱的前端模板也是可行的方案。

转回来,如果你去看百度翻译的修补方法,并没有采取上面的方式(如在写入innerHTML时做escape),而是,在ajax输出时,确保字段内容是escape过的。这跟前面说的“存储型”漏洞的那种看似可行的方案类似。当然,这个ajax调用作为百度翻译的一个内部接口来说,是可以内部约定某个字段的格式本身是html——这比限制数据库字段必须是html要好多了——毕竟如果有需求的话你可以再开个接口返回文本而不是html。但是从大原则来说,这仍然是一个糟糕的选择。比如,如果这是一个外部(第三方)接口——假设百度翻译直接通过CORS XHR读取了360百科的内容——就是不可接受的。一来,360百科未必给你想要的格式,二来,也是更重要的,你的安全是基于别人的接口返回是否正确(或者不被黑)的基础上,你说行不行?


好了,扯了那么多,最重要的一点就是,不要光知道几个名词的差别。作为程序员,更重要的是知道原理,看清本质,才能不被名词所迷惑。



【更新:关于PHP中 json_encode 存在的问题,请直接看我写的 safe_json_encode ——

https://gist.github.com/hax/75706bac6d89dfeb7195

。注意gist最近被DNS污染了,请自行使用DNSCrypt或直接翻墙访问,并预祝病魔早日战胜方前校长100次。】

单单解释三者的区别,似乎有点单调。说到xss,就必须要提一提js,脱离了js去谈xss都是耍流氓!

1,)先来分析一下LZ说的DOM-based XSS。

一句话概括:DOM—based XSS漏洞是基于文档对象模型Document Objeet Model,DOM)的一种漏洞。

如果楼主没有搞懂dom树的关系,那对dom xss的了解是非常有限的。

先以一张w3c的图来说明,到底什么是dom:


哦..原来是这样(似乎你有点了解?),没错,dom就是一个树状的模型,你可以编写Javascript代码根据dom一层一层的节点,去遍历/获取/修改对应的节点,对象,值。

了解了这么一个知识点,你就会发现,其实dom xss并不复杂,他也属于反射型xss的一种(2016.3.16修改,domxss取决于输出位置,并不取决于输出环境,因此domxss既有可能是反射型的,也有可能是存储型的),简单去理解就是因为他输出点在DOM,所以在道哥的《白帽子讲Web安全里》也有详细介绍。dom - xss是通过url传入参数去控制触发的。

2,)分析完dom-xss之后,再说说存储型xss,其实也很好理解,存储型xss,自然就是存入了数据库,再取出来,导致的xss。

3,)反射型xss实际上是包括了dom - xss了,关键点仍然是在通过url控制了页面的输出(dom-xss也类似,只因为输出地点不同而导致结果不一致)。

说了这3种区别,不能仅仅停留在理论上,下面我贴出这三种xss代码的demo【均以php为例】:

dom-xss:

<?php
error_reporting(0);
$name = $_GET["name"];
?>
<input id="text" type="text" value="<?php echo $name;?>" />
<div id="print"></div>
<script type="text/javascript">
var text = document.getElementById("text"); 
var print = document.getElementById("print");
print.innerHTML = text.value; // 获取 text的值,并且输出在print内。这里是导致xss的主要原因。
</script>

前面有一个热心的回答者说到一点:domxss需要关闭ie xss过滤器才会触发。实际上是不需要的,domxss是可以在各个浏览器触发,截图说明(基于上面那段代码):

chrome:

ie(开启了xss防护模式):

存储型xss:

<?php
error_reporting(0);
$name = $_GET["name"];
//连接服务器
$conn = mysql_connect("127.0.0.1","root","");
//打开数据库
mysql_select_db("test",$conn);
//执行SQL
mysql_query("set names 'utf8'");
$sql_insert = "insert into liuyan(id,content) values('$id','$name')";
$result = mysql_query($sql_insert,$conn);
$sql_select = "select * from liuyan";
$results = mysql_fetch_array(mysql_query($sql_select));
echo $results[content];
?>

上面这段代码写的比较简洁,可以看到用户可控的$_GET name直接被带入到数据库中,随后被查询并且输出出来了,直接导致了xss。感兴趣的同学可以做下测试,先通过

http://localhost/2.php?name=%3Cscript%3Ealert(1)%3C/script%3E

然后再访问

localhost/2.php

。即可触发存储型xss:

步骤一,是为了让xss代码写入数据库,步骤二是为了把恶意代码从数据库取出来并且输出在页面上。

反射型xss:

<?php
$name = $_GET["name"];
?>
<input type="text" value="<?php echo $name?>">

结果:

好了,花了这么长的时间去说明,其实想告诉题主:

在易用上,存储型XSS > DOM - XSS > 反射型 XSS。

为什么这么说?因为存储型xss最持久,而且更为隐蔽,因为是存在数据库当中的,触发的url当中没有带js或者其他的html代码。dom-xss,排在其次。为何?上面的图最直观:因为它能绕过大部分浏览器的过滤(新版Chrome针对script context类型的domxss做了检测拦截)。 反射型的xss还要深思熟虑的考虑根据浏览器去bypass各种过滤,易用性稍微差一些。注:反射型xss和dom-xss都需要在url加入js代码才能够触发。

在检测上,目前我还没有看到比较好的针对存储型xss的检测手段,而domxss和反射型xss均有比较好的检测手段了。

其实这个问题早已有人问过了:

存储型XSS与反射型XSS有什么区别?

黑哥的总结是交互,这点我也是赞同的,这个是相同点。

希望我这个答案是升级版,能帮助到新手学习xss,这样我码字这么多也是值得的:)

资料参考:

HTML DOM 教程

学习教程:

白帽子信息_心伤的瘦子

2014-11-17修改:

在新版本的Chrome已经能够拦截scrip context类型的dom-xss,感谢

@ice png

指正错误,我在Chrome的老版本亲测是可行的:)版本号10.0.648.6