跨站脚本攻击

跨站脚本攻击,Cross-site scripting, 缩写 XSS, ( 以前的文档也有写作 CSS 的,但跟层叠样式表的缩写一样,为了区分,现在一般称作 XSS ),是一种非常常见的网络攻击方式,类似于 SQL 注入,也是一种注入攻击。

XSS 通过在网页中插入危险脚本,可以绕过同源策略之类的安全机制,获得执行危险操作的权限,达到攻击的目的。这些攻击可能涵盖访问敏感信息、篡改 HTML 内容、跳转到恶意网站、监控用户输入等等等,具有相当大的威胁。

攻击的逻辑

  • 对于服务器来说,攻击代码是是一种普通数据(如一段 HTML 字符串),对服务器本身是没有特殊意义的,不会对服务器的行为有影响
  • 对于客户端来说,攻击代码(如 HTML )是一种有特定意义的指令,会影响客户端的行为
  • XSS 攻击通常会在客户端传递经过精心设计的恶意内容给服务器,这些数据会影响服务器正常组装 HTML、JavaScript、CSS 等指令代码的过程,让最终组装出来的指令代码包含恶意内容。

比如,通过传递经过巧妙设计的 URL 查询参数,让服务器使用该参数组装 HTML 标签时,意外提前闭合 HTML 标签,然后接续上一段攻击代码,这些包含攻击代码的 HTML 在浏览器上就反映为攻击。

XSS 的类型

XSS 根据特点,可以分为几种类型。

反射型 XSS ( Reflected XSS )

反射型 XSS,即非持久型( non-persistent ) XSS,主要是由于服务器端收到客户端的不安全输入(如一个未验证的 URL 查询参数),在客户端触发从而发起攻击。

过程:

  1. 客户端提交有害输入
  2. 服务器接收有害输入
  3. 服务器组装出包含攻击的代码
  4. 客户端执行包含攻击的代码

反射型 XSS,仅在当次访问中生效,一般是在哪个页面攻击,就在该页面生效,注入的恶意代码,不会影响到其他网页。

例如 URL 参数中包含了攻击代码,若服务器不加过滤地使用了这些攻击代码,组装成的 HTML并发送给浏览器解析执行,一旦用户访问,就会遭受攻击。

假如访问:http://xssexample.com/?name=world 时,输出

1
Hello world

其中 world 从 URL 查询参数name=world中得到,如果服务器不严格检测name参数的值,那么就存在 XSS 漏洞,例如,攻击者诱导用户访问:

http://xssexample.com/?name=<script>alert('xss')</script>

时,就会弹出一个内容为 xss 的警告框,因为这时候输出的 HTML 是这样的:

1
Hello <script>alert('xss')</script>

这只是个恶作剧性质的攻击代码,如果攻击者注入的是带着不可告人目的的恶意代码,那么后果不堪设想。

存储型 XSS ( Stored XSS )

通常漏洞发生在用户提交的文章、留言评论、资料填写等需要存储到服务器的场景,服务器如果没有做严格的输入检查,就将内容不做处理地持久化存储,就可能存储了危险代码,用户访问这些内容的时候就形成了攻击。

存储型 XSS,本质上根反射型 XSS 并无区别。只是需要事先向服务器提交恶意数据,将数据做持久化存储,用户访问受影响的页面时,服务器从持久层拿数据,并组装成带攻击的 HTML 页面。因此相比反射型 XSS,仅仅多了一个持久化的环节。

过程:

  1. 客户端提交有害输入
  2. 服务器存储有害输入
  3. 服务器取出有害输入
  4. 服务器组装出包含攻击的代码
  5. 客户端执行包含攻击的代码

效果上,存储型 XSS,攻击页面跟受攻击影响的页面之间没有绑定关系,因为同样一个数据,可能在页面多个地方、在多个页面中使用。例如,一个用户名字,可能在会员中心、列表、详情等等页面都会用上,所以,影响更广泛。

基于 DOM 的 XSS ( DOM Based XSS )

这是一种特殊的反射式 XSS。只是不是反射自服务器,而是反射自客户端 JavaScript,使用客户端 JavaScript 代替服务器的作用,全程在客户端实现攻击。

过程:

  1. 客户端提交有害输入
  2. 客户端 JavaScript 处理有害输入
  3. 客户端 JavaScript 组装出包含攻击的代码
  4. 客户端执行包含攻击的代码

例如一个页面中,有一个 AJAX 获取外部数据并输出到页面的功能,如果 AJAX 获取的数据源是不可信的( 例如是来自第三方数据接口 ),并且输出数据的时候,也没有做严格的检查和处理( 例如直接作为 innerHTML 插入 ),则存在 XSS 风险。攻击者只需要在数据源中添加有害的数据,那么,受影响的页面就会输出攻击代码。

这种攻击方式更难以察觉和定位问题。

XSS 攻击输出点

XSS 攻击代码,可能出现在 HTML 文件中的许多地方。

HTML 标签之间

1
2
3
<div> XSS </div>
<script> XSS </script>
<style> XSS </style>

HTML 注释里

1
<!-- XSS -->

HTML 标签名

1
<XSS>...</XSS>

HTML 标签属性名

1
<div XSS=""></div>

HTML 标签属性值

1
<div name="XSS"></div>

防御攻击

核心思想是,不信任用户的输入,对所有安全性无法保证的数据,做好输入输出的检查、过滤。在需要插入安全性无法确保的数据的地方,要将数据做相应地编码处理。

对于不同类型的插入内容,需要做的编码规则也各不相同。

HTML 标签之间的内容的编码

对于 HTML 标签之间的插入内容,需要对几个敏感的字符做 HTML Entity 编码。

安全原则:

  1. & 编码为 &amp;
  2. < 编码为 &lt;
  3. > 编码为 &gt;
  4. " 编码为 &quot;
  5. \ 编码为 \
  6. / 编码为 /

HTML 标签属性值的编码

插入内容到 HTML 属性值里的时候,需要对内容做 HTML 属性编码。

安全原则:

  1. 阿拉伯数字和字母无需编码
  2. ASCII 码小于 256 的字符,按照 &#xHH; 的形式编码,HH 对应该自负的十六进制数字
  3. 属性值部分应该正确使用引号包裹,否则较容易被提前闭合造成 XSS 漏洞,若没有引号,又没有进行上面第2点提到的编码,那么攻击者只需要插入一个空格即可提前闭合该属性,并紧接着插入攻击脚本。然后, % * + , / ; < = > ^ | `(IE将反单引号当做单引号) 等符号也可用来闭合属性。如下例
1
<div width=$var></div>

那么,攻击者只需要输入:10 onmouseover="javascript:alert('xss')" ,即可闭合掉该属性,并额外插入一段攻击代码。最终输出到浏览器的 HTML 是:

1
<div width=10 onmouseover="javascript:alert('xss')"></div>

script 内容的编码

对于插入的 JavaScript 脚本,包括在 HTML 标签的事件处理属性的属性值部分( 如 onclick ),要做 script 编码。

安全原则:

  1. 阿拉伯数字、字母无需编码
  2. ASCII 码小于 256 的字符,以 \xHH 的形式编码,HH 对应该字符的十六进制数字
  3. 只允许这些编码后的内容,插入在用引号括起来的值的部分,如:
1
2
3
  <script>
  var insert = "<%= encodeJavaScript(@INPUT) %>";
  </script>
  1. 注意像 setInterval 之类的函数,就算进行了 JavaScript 编码,依然存在 XSS 漏洞
  2. 仅用反斜杠 \ 转义是不可靠的,因为浏览器会先进行 HTML 解析,再做 JavaScript 解析,像用反斜杠转义的双引号,可能会被当作 HTML 字符解析,从而出现 XSS 漏洞,如下例
1
2
3
  <script>
  var a = "$var";
  </script>

攻击者输入 \"; alert('xss');// ,即可将最终 HTML 输出变成:

1
2
3
  <script>
  var a = "\\"; alert('xss');//";
  </script>

浏览器在解析的时候,会认为反斜杠后面的那个双引号和第一个双引号相匹配,继而认为后续的 alert('xss') 是正常的 JavaScript 脚本。

样式表内容编码

当需要往 style 标签或者 style 属性里插入不可信数据的时候,需要对这些数据进行 CSS 编码。

安全原则:

  1. 阿拉伯数字和字母不用编码
  2. ASCII 码小于 256 的字符以 \HH 的形式编码,HH 为该字符对应的十六进制数字
  3. 只插入到 CSS 属性的值部分
  4. 最好不要放到 url, behavior 等等复杂属性里,还有 IE 的 CSS 表达式中也要尽量避免

URL 内容的编码

当需要插入安全性无法保证的 URL 时,需要进行 URL 编码。

安全原则:

  1. 阿拉伯数字和字母不用编码
  2. ASCII 码小于 256 的字符以 %HH 的形式编码,HH 为该字符对应的十六进制数字
  3. URL 属性应该用引号包围内容,避免被轻易闭合
  4. 不对整个URL进行编码,而要根据插入的位置( 如 hrefsrc 等等 ),进行恰当的起始部分的协议字段的验证。( 如 data 协议、javascript 伪协议等等)