资讯专栏INFORMATION COLUMN

【译】Excess-XSS 一份关于 XSS 的综合教程

timger / 3449人阅读

摘要:示例攻击如何进行下图展示了攻击者如何进行攻击攻击者利用网站的表单插入恶意字符串到网站数据库中。恰恰相反,至少有两种常见的方式,会导致受害者发起针对自己的反射型攻击。攻击者精心构造了一个包含恶意字符串的,将其发送给受害者。

原文地址:http://excess-xss.com/。如有翻译不当之处,欢迎指出 :D

分为四部分:

概述

XSS 攻击

XSS 防御

总结

第一部分:概述 XSS 是什么

跨站脚本攻击(XSS)是一种代码注入攻击,攻击者利用它可以在其它用户浏览器中执行恶意 JavaScript。

攻击者并不是直接面对受害者。而是,为了让网站替自己传输恶意 JavaScript,攻击者需要利用受害者访问的网站上的漏洞。对受害者的浏览器而言,恶意的 JavaScript 似乎是网站合法的一部分,网站在无意中成为了攻击者的共犯。

恶意 JavaScript 是如何被注入的

攻击者在受害者浏览器中执行恶意 JavaScript 的唯一方式就是将其注入到受害者浏览的网页中。如果网站直接将用户输入显示到页面中,就可能导致恶意代码注入,因为攻击者可以插入恶意字符串,让受害者浏览器误以为是代码。

在下面的例子中,一段简单的服务端脚本用来在网页中展示最新的评论:

print ""
print "Latest comment:"
print database.latestComment
print ""

上面的脚本认为评论只会包含文本。然而,由于是直接展示用户输入,攻击者可以提交类似这样的评论 。现在,访问该页面的所有用户就会接收到如下响应:


Latest comment:

当用户浏览器加载了该页面,就会执行 ,这表明,不管实际中执行的是什么代码,只要攻击者注入的脚本出现,问题就存在。

第二部分:XSS 攻击 XSS 攻击涉及到的角色

在详细介绍 XSS 攻击如何进行之前,需要定义 XSS 中涉及到的角色。通常,一次 XSS 会涉及三个角色:网站受害者攻击者

网站响应 HTML 页面给发起请求的用户。在我们的例子中,网站是 http://website/。

网站的数据库用来存储一些用户的输入,并输出到网站页面中。

受害者是网站的普通用户,通过浏览器请求页面。

攻击者是网站的恶意用户,准备利用网站的 XSS 漏洞发起攻击

攻击者的服务器是由攻击者控制,唯一的用途是盗取用户敏感信息。在我们的例子中,位于 http://attacker/

一次攻击示例

在这个例子当中,我们假设攻击者的目标是通过利用网站 XSS 漏洞盗取受害者的 cookie。这可以通过在受害者浏览器中执行如下代码实现:

上面的脚本将浏览器导航到新的页面,并触发一次 HTTP 请求到攻击者的服务器。URL 将受害者的 cookie 作为查询参数,当请求到达攻击者服务器时,攻击者就可以提取 cookie。一旦攻击者拿到了 cookie,他就可以冒充受害者,进而发起更深入的攻击。

从现在起,我们将上面的 HTML 代码称为恶意字符串或是恶意脚本。需要注意的是,上面的字符串只有被受害者浏览器作为 HTML 解析之后,才会产生危害。

示例攻击如何进行

下图展示了攻击者如何进行攻击:

攻击者利用网站的表单插入恶意字符串到网站数据库中。

受害者从网站中请求页面。

网站在响应中包含了来自数据库的恶意字符串,并返回给受害者。

受害者的浏览器执行了响应中的恶意字符串,将受害者的 cookie 发送到了攻击者的服务器。

XSS 类型

尽管 XSS 攻击的目标都是在受害者浏览器中执行恶意 JavaScript,还是有几种完全不同的方式来实现该目标的。XSS 攻击通常可以分为三类:

存储型 XSS(Persistent XSS),恶意字符串来自网站数据库

反射型 XSS(Reflected XSS),恶意字符串来自受害者的请求

DOM 型 XSS(DOM-based XSS),漏洞位于客户端代码而不是服务端代码

前面的例子展示了存储型 XSS 攻击。接下来介绍另外两种:反射型 XSS 和 DOM 型 XSS。

反射型 XSS

在反射型 XSS 中,恶意字符串是受害者向网站发起的请求的一部分。然后,网站将包含恶意字符串的响应返回给了受害者。如下图所示:

攻击者精心构造了一个包含恶意字符串的 URL,将其发送给受害者

攻击者欺骗受害者,使其访问了该 URL

网站在响应中包含了来自 URL 的恶意字符串

受害者浏览器执行了响应中的恶意字符串,将自己的 cookie 发送到了攻击者的服务器

如何成功发起反射型 XSS 攻击

首先,反射型 XSS 看起来是无害的,因为它需要受害者亲自发起一个包含恶意字符串的请求。而没有人会自愿攻击自己,乍看起来是没办法发动攻击的。

恰恰相反,至少有两种常见的方式,会导致受害者发起针对自己的反射型 XSS 攻击。

如果目标用户是特定的某个人,攻击者可以发送恶意 URL 给受害者(例如:使用邮件或是即时消息),并欺骗受害者访问该 URL。

如果目标用户是一大群人,攻击者可以发布指向恶意 URL 的链接(例如:在自己的网站或是社交网络上),并等待受害者点击。

这两种方式类似,并且都可以通过利用短网址服务来提高成功率,短网址会使得恶意字符串难以分辨。

DOM XSS

DOM XSS 是存储型 XSS 和 反射型 XSS 的变种。在 DOM XSS 攻击中,一直到页面运行了 JavaScript,恶意字符串才被实际的解析。
下图展示了 DOM XSS 攻击。

攻击者精心构造了一个包含恶意字符串的 URL,将其发送给受害者。

攻击者欺骗受害者,使其访问了该 URL

网站接收到响应,但是响应中并不包含恶意字符串

受害者浏览器执行响应中合法的 JavaScript,导致恶意代码插入到了页面中

受害者浏览器执行了插入到页面中的恶意代码,将 cookie 发送到了攻击者的服务器

DOM XSS 的不同之处在哪

在前面存储型和反射型 XSS 示例中,服务端把恶意脚本插入到页面中,然后将其作为响应发送给受害者。当受害者浏览器接收到响应时,会把恶意脚本当作是页面的合法内容,在页面加载过程中与其它脚本一同执行。

在 DOM XSS 示例中,恶意脚本并没有插入到页面中,在页面加载过程中,只有合法的 JavaScript 被执行。问题在于,合法的 JavaScript 直接将用户输入插入到页面中。因为恶意字符串通过 innerHTML 插入到页面中,就会作为 HTML 解析,导致恶意脚本执行。

两者的区别很小但是很重要:

在传统的 XSS 中,恶意 JavaScript 作为服务端发送页面的一部分,在页面加载时被执行

在 DOM XSS 中,恶意 JavaScript 在页面加载完成后某个时间点执行,是由页面合法 JavaScript 没有安全处理用户输入造成的。

为什么 DOM XSS 很重要

在上面的 DOM XSS 例子中,JavaScript 并不是必须的;服务端就可以生成完整的 HTML。如果服务端代码不存在漏洞,那么网站就不存在 XSS。

但是,随着 WEB 应用越来越高级,越来越多的 HTML 是在客户端通过 JavaScript 生成而不是在服务端生成。任何时候不刷新整个页面,需要更新内容,就必须通过 JavaScript 进行。值得注意的是,AJAX 请求后更新页面就是这样的例子。

也就是说,XSS 漏洞不仅可以出现在网站服务端代码,还可能出现在客户端 JavaScript 代码中。结果就是,即使服务端代码完全没问题,在页面加载完成后,客户端代码还是可能在 DOM 更新中不安全的包含了用户输入。一旦发生,客户端代码就存与服务端无关的 XSS 漏洞。

对服务端不可见的 DOM XSS

有一种特殊的 DOM XSS,恶意字符串不会被发送到网站服务端:当恶意字符串位于 URL 的片段标识符中(# 号之后)。浏览器不会发送 URL 的片段标识符到服务端,这样,服务端代码就没方法获取它。虽然客户端代码可以获取它,但是如果没有进行安全处理就会出现 XSS 漏洞。

并不是只有片段标识符会出现这种情况。对服务端不可见的用户输入还包括 HTML5 的新特性,例如:LocalStorage 和 IndexedDB。

第三部分:防御 XSS 防御 XSS 的方法

记住,XSS 攻击是一种代码注入:用户输入被错误的解释为恶意程序代码。为了防止这种类型的代码注入,需要对输入进行安全处理。对 web 开发者而言,可以采用两种不同的方式:

编码,转义用户输入,这样浏览器就只会将其解释为数据而不是代码。

校验,过滤用户输入,这样浏览器就可以将其解释为没有恶意命令的代码。

尽管这两种方式有着本质的区别,但是它们还是存在共同点,在使用过程中,理解这些共同点很重要:

上下文:根据用户输入在页面中插入位置的不同,需要进行不同的安全处理

流入/流出(inbound/outbound):安全处理要么在网站接收输入时(inbound)进行,要么在网站将输入到插入页面之前(outbound)进行

客户端/服务端:安全处理可以在客户端进行,也可以在服务端进行。在某些情况下,需要在客户端和服务端同时进行安全处理。

在详细介绍如何进行编码和校验之前,我们先来逐个解释以上三点。

输入处理的上下文

在网页中,用户输入可能插入的地方有很多。每个地方的上下文不同,要遵循指定规则才能保证用户输入不会打破上下文,被解释为恶意代码。下面是常见的上下文:

上下文 示例代码
HTML element content
userInput
HTML attribute value
URL query value http://example.com/?parameter=userInput
CSS value color: userInput
JavaScript value var name = "userInput";
为什么上下文很重要

如果用户的输入在编码和校验之前就插入到页面中,在上面提到的上下文中都可能产生 XSS 漏洞。攻击者要想注入恶意代码,他只需插入对应上下文的结束分隔符,紧接着是恶意代码。

例如,如果某个站点将用户输入直接插入到 HTML 属性中,攻击者通过以引号作为开头的输入就可以注入恶意脚本,如下所示:

Application code
Malicious string ">

只需移除用户输入中的所有引号就可以避免上面的 XSS,但这只对该上下文有效。如果相同的输入插入到其它的上下文,结束分隔符可能就不同,又可能导致注入。所以说,需要根据用户输入插入点的上下文,进行不同的安全处理。

流入/流出(inbound/outbound)的输入处理

仅凭直觉,似乎只需在网站接收到用户输入时,对其进行编码或是校验就可以防止 XSS。用这种方式,任何恶意代码插入到页面中的时候都已经失效了,生成 HTML 的脚本就无需关心安全处理。

问题在于,前面也提到过,用户输入可以插入到页面中很多地方。确定用户输入最终会插入到哪一种上下文并不简单,而且相同的用户输入经常需要插入到不同的上下文。依赖 inbound 输入处理来防御 XSS 是一种非常脆弱的方案,很容易出错。(PHP 废弃的特性 magic quotes 就是这样的方案)

反倒是,outbound 输入处理应该作为防御 XSS 的首要阵线,因为它可以考虑到用户输入即将插入的特定上下文。换句话说,inbound 校验可以用来作为第二层保护,后面会再提到。

在哪里进行安全处理

当前,在大多数 web 应用中,客户端代码和服务端代码都会涉及到处理用户输入。为了防御所有类型的 XSS,安全处理也要同时在服务端和客户端进行。

为了防御传统的 XSS,安全处理必须在服务端代码进行。通过服务器支持的语言实现。

为了防御服务器接收不到恶意字符串的 DOM XSS,(例如,前面的提到的片段标识符攻击)安全处理必须在客户端进行。通过 JavaScript 实现。

现在我们已经解释过为什么上下文很重要,为什么区分 inbound 和 outbound 输入处理很重要,为什么安全处理需要同时在客户端代码和服务端代码同时进行。接下来,将要阐述两种安全处理(编码和校验)是如何进行的。

编码

编码用来转义用户输入,这样浏览器就会将其解释为数据而不是代码。在 web 开发中最常见的就是 HTML 转义,它会把字符 <> 分别转换为 <>

下面的伪代码展示了服务端代码如何对用户输入进行 HTML 转义,然后将其插入到页面中:

print ""
print "Latest comment: "
print encodeHtml(userInput)
print ""

如果用户输入为字符串 ,输出 HTML 结果如下:


Latest comment:

由于所有有特殊含义的字符都被转义了,所以浏览器不会把用户输入当作 HTML 解析。

在客户端和服务端进行编码

当在客户端进行编码时,使用的是 JavaScript,它包含针对不同上下文对数据进行编码的函数。

当在服务端进行编码时,可用的函数和服务端的语言和框架有关。考虑到服务端语言和框架多种多样,该教程不会涉及特定语言或框架的编码。不管怎样,熟悉客户端编码函数对编写服务端代码也是很有帮助的。

在客户端编码

当在客户端使用 JavaScript 编码用户输入时,有一些内置的方法和属性会自动根据上下文进行编码:

上下文 方法、属性
HTML element content node.textContent = userInput
HTML attribute value element.setAttribute(attribute, userInput) or element[attribute] = userInput
URL query value window.encodeURIComponent(userInput)
CSS value element.style.property = userInput

前面提到的最后一种上下文(JavaScript values)并不包含在内,because JavaScript provides no built-in way of encoding data to be included in JavaScript source code.

编码的局限性

即使进行了编码,在一些上下文中还是可能输入恶意字符串。一个值得注意的示例如下,当用户输入用来提供 URL:

document.querySelector("a").href = userInput

尽管给一个超链接元素的 href 属性赋值,会自动编码保证其成为属性值而不是其它,但这并不能阻止攻击者插入以 "javascript:" 开头的 URL。当点击链接时,URL 中嵌入的 JavaScript 就被执行。

当你希望用户可以自定义页面部分代码时,编码这种处理方式就不太合适了。例如:在个人资料页,用户可以自定义 HTML。如果对这些 HTML 进行编码,那么个人资料页只能包含纯文本了。

在这种情况下,编码必须配合校验进行,下面介绍校验。

校验

校验是指过滤用户输入,以便移除所有恶意的部分,而无需移除其中所有代码。在 WEB 开发中最常见的一种校验就是允许一些 HTML 元素(例如: )但是不允许其它一些(例如:

如果定义了合理的 CSP 策略,浏览器就不会加载和执行 malicious-script.js,因为 http://attacker/ 不会出现在可信源集合中。在这种情况下,即使网站没能正确对输入进行安全处理,CSP 策略可以防止漏洞造成任何损害。

即使攻击者注入的不是外部文件,而是行内脚本代码,合理的 CSP 策略仍可以禁止行内 JavaScript,从而防止漏洞造成任何伤害。

如何启用 CSP

默认情况下,浏览器不强制使用 CSP。为了给网站开启 CSP,响应中要带上额外的 HTTP 头:Content-Security-Policy。如果浏览器支持 CSP,那么所有带有这一 HTTP 头的页面都会遵守 CSP。

由于安全策略是通过每一个 HTTP 响应来发送,可以在服务端对每一页都设置安全策略。相同的策略可以通过在每个响应中提供相同的 CSP 头来应用到整站上。

Content-Security-Policy 响应头的值是一个字符串,定义了一个或多个安全策略,会应用到页面中。后面会讲到它的语法。

注意:本节示例中,为了表示清楚 HTTP 头部,使用了换行和缩进,在实际的 HTTP 头部中,这是不应该出现的。

CSP 语法

CSP 头部语法如下:

Content‑Security‑Policy:
    directive source‑expression, source‑expression, ...;
    directive ...;
    ...

由两个元素构成:

指令:声明资源类型的字符串,从预定义列表中取值。

来源表达式(Source expressions):描述可以用于下载资源的一个或多个服务器的模式

对每个指令来说,给定的来源表达式定义了该资源类型可以使用哪些来源下载资源。

指令

在 CSP 头部中可以使用的指令列表如下:

connect‑src

font‑src

frame‑src

img‑src

media‑src

object‑src

script‑src

style‑src

除了上面这些,还有一个特别的指令:default-src,它可以用来给所有指令提供一个默认值,那些没有出现在 HTTP 头部中的指令使用默认值。

来源表达式(source expressions)

来源表达式语法如下:

protocol://host‑name:port‑number

主机名可以以 打头,表示所提供的主机名的任何子域都被允许。类似的,端口号也可以是 ,表示所有端口都被允许。此外,协议和端口号可以省略。也可以只指定协议,从而实现要求所有资源都通过 HTTPS 加载。

除此之外,来源表达式还可以是下面四个有特殊含义的关键词之一(包含引号):

"none":不允许该类型资源

"self":允许从提供当前页面的主机下载资源

"unsafe-inline":允许在页面中嵌入资源,例如:行内

阅读需要支付1元查看
<