首发于 水滴前端

给XSS加点S

TOC:

约定

  • 本文中出现由花括号包裹的内容为服务端进行 html 字符串拼接的动态内容:{ userData },在其他文章中经常表现为 <%= userData >。
  • 本文中所使用的关键词:“用户数据”,与“非受信数据”同义。

XSS 简介

XSS 是一种代码注入攻击,在受害者浏览器上注入恶意代码并执行,本质是前后端渲染不受信任的用户数据导致的安全问题。

不受信任的用户数据指的是由用户提供的数据,例如:用户在表单中输入的值,用户在 URL 中拼接的字符串等等。通常表现为恶意的 JavaScript 脚本或 CSS 脚本。

XSS 种类

通常我们把 XSS 分为三大类:存储型、反射型 以及 DOM 型。

存储型和反射型是服务单渲染(SSR)所导致的安全问题。

而 DOM 型则是客户端渲染(CSR)所导致的安全问题。

存储型【 Stored XSS (AKA Persistent or Type I)】

1、顾名思义,攻击者提交的恶意数据被存储在目标站点的服务器(如数据库中)

2、受害者访问目标站点,服务端没有安全的处理用于拼接 HTML 的数据,并将有问题的 HTML 发送给浏览器

3、恶意代码在受害者浏览器中执行,受到攻击

反射型【 Reflected XSS (AKA Non-Persistent or Type II)】

1、反射型与存储的区别在于,反射型是非持久性的,大多是同构 URL 的参数进行恶意代码的注入

2、受害者点击由攻击者预先设计好的恶意 URL,跳转到目标站点

3、目标站点从 URL 中取出恶意数据并拼接 HTML,再发送给浏览器

4、恶意代码在受害者浏览器中执行,受到攻击

DOM 型【 DOM Based XSS (AKA Type-0)】

第一个提出 DOM 型 XSS 攻击的是 Amit Klein ,DOM 型不需要服务端参与,是纯前端安全问题。从恶意数据源 到 接受并处理恶意数据的接收器都在浏览器中。

其中恶意数据源包括不限于:URL(如:document.loaction.href),HTML 元素等。

处理恶意数据的接收器如 document.write()、innerHTML、setTimeout/setInterval、eval 等等。

小结

1、如果你的项目是服务端渲染,又因为服务端渲染的内容几乎不会是纯静态的,因此我们需要注意存储型、反射型以及DOM型攻击,这些都有可能发生。

2、如果你的项目时客户端渲染,那么只需要注意 DOM 型攻击即可。


XSS 的危害

用户提供的数据,我们应该始终作为不受信任的数据处理,一旦把不受信任的数据在浏览器中执行,例如一段 JavaScript 脚本,那么该脚本将拥有完全的控制能力,它可以盗取用户私密信息,例如 cookie 等,可以改变站点的样式从而诱导“点击劫持”,这段恶意脚本可以代表用户执行任何操作。

存储型和反射型 XSS 的防御

防御 XSS 攻击没有想象的容易,但也没有想象的那么难,存储型和反射型需要服务端参与,我们接下来就讨论一下如何防御这类攻击,并给出 Vue SSR 的情况下是否会有某些问题,当然接下来介绍的某些攻击手段不仅仅会存在于 SSR 中,在 CSR 中也是有问题的,我们都会提及。

首先我们要明确一件事儿,任何安全问题都是在错误的信任用户提供的数据所导致的,因此,任何用户提供的数据都应该被特殊对待,在必要的情况下做正确的处理并展示。

1、插入到 html 标签内的用户数据

案例:

<div>{ userData }</div>

如果 userData 的内容是 '<script>alert(document.cookie)</script>'。那么最终拼接而成的字符串将是:

<div><script>alert(document.cookie)</script></div>

很显然,浏览器会弹出窗口,并展示 cookie。

防御方式:

在将 userData 展示给用户之前,要对其进行 html 转义(escape),既将如下字符转移成对应的 html 实体:

  • & → &amp;
  • < → &lt;
  • > → &gt;
  • " → &quot;
  • ' → &#x27;
  • / → &#x2f;

这样,转义后的 html 将变成:

<div>&lt;script&gt;alert(document.cookie)&lt;&#x2fscript&gt;</div>

Vue 的 SSR 或 CSR 是否存在这个问题?

无论是 SSR 还是 CSR,如果是用模板插值,即 {{}},是不存在问题的,Vue 会对数据做 html 转义,但如果使用 v-html 指令,则会存在此问题。

2、作为普通标签属性值的用户数据

这里的普通标签属性,指的是非 href/src/style 以及事件属性(例如 onlick 等)之外的其他属性。

案例:

<div value={ userData }></div>

如果 userData 的内容为:

userData = '"" onclick=alert(document.cookie)'

那么最终生成的 html 内容将是:

<div value="" onclick=alert(document.cookie)></div>

很明显,产生了注入,可以发现,userData 原本应该作为 value 属性的属性值,但这段特殊的字符串导致它能够打破作为 value 属性的属性值这一限制,这其实是产生注入的常见手段。

防御方式:

如果仅仅对 userData 进行 html escape 是不够的,并不能防止这类攻击,因此我们的防御手段应该是:

转义所有非 数字或字母 的字符为 &#xHH; 这种格式,其中 HH 代表 16 进制值或者命名引用。

这样我们最终产生的字符串如下:

<div value=&quot;&quot;&nbsp;onclick&equals;alert&lpar;document&period;cookie&rpar;></div>

这段代码会被浏览器渲染为如下内容:

可以看到整个字符串都作为 value 的属性值处理。

Vue 的 SSR 或 CSR 是否存在这个问题?

SSR:不存在,Vue 在拼接数据时会对其进行 html escape

CSR:不存在,Vue 内部在设置节点的属性值时使用如 setAttribute 这样的浏览器 API,这原生避免了此类问题

因此如下代码无论是 SSR 还是 CSR 都不会导致 XSS:

<template>
	<div :id="val"></div>
</template>
<script>
export default {
	data() {
		return { val: '" onclick="alert(document.cookie)' }
</script>

3、作为“特殊 html 属性的属性值”的用户数据

这里所谓的特殊 html 属性,指的就是 href/src/style 等基于 URL 的属性以及事件属性(例如 onclick 等),我们先来看一下 href 属性。

案例一【非法协议 javascript:alert(xxx)】:

<a href={ userData }></a>

假设 userData 为:

userData = 'javascript:alert(document.cookie)'

那么最终的 html 字符串将是:

<a href=javascript:alert(document.cookie)></a>

嗯,又产生了注入,而且,即便把这段字符串中的非数字和字母的字符进行转义,也避免不了问题:

<a href=javascript&#x3a;alert&#x28;document.cookie&#x29;></a>

这仍然是问题代码。

防御方式:

采用协议白名单,对 userData 做严格校验:

const allowed = ['http', 'https']
const valid = isValid(userData, allowed)
// ...

或者使用开源的库做过滤,如 npmjs.com/package/@brai

Vue 的 SSR 或 CSR 是否存在这个问题?

SSR 和 CSR 都存在问题,如下代码即会产生此问题:

<template>
	<a :href="val"></a>
</template>
<script>
export default {
	data() {
		return { val: 'javascript:alert(document.cookie)' }
</script>

案例二【用户数据作为完整的 URL】:

还是刚才的例子,如下:

<a href={ userData }></a>

整个 url 内容全部由 userData 提供,而且我们注意到 href 属性值是 unquoted 的,所以 userData 很容易打破“作为属性值”这一特征,例如 userData 的内容是:

userData = '"" onclick=alert(0)'

那么最终生成的 html 字符串为:

<a href="" onclick=alert(0)></a>

但是如果属性值被单引号或者双引号引用起来的话,问题会变得简单一些,因为只要相应的单或双引号才能打破上下文:

<a href='"" onclick=alert(0)'></a>

如上,href 属性值被 单引号引用。

但问题是如果 userData 中也包含单引号,那么就又产生了问题,考虑到属性值可以省略引号,因此我们还不能仅仅处理 userData 中的单双引号就觉得一切 OK 了。

防御方式:

对于 href/src 这种期望属性值为 url 的属性,正确的处理方式是:

  • 1、利用协议白名单,排除类似 javascript: 等协议。
  • 2、在 1 的基础上对 userData 进行 URI 编码,如果在 JavaScript 中,使用 encodeURI 可以对完整 URL 进行编码。
  • 3、对 URI 编码后的内容再进行一次 html escape ,以便将引号 "' 等内容进行转义,防止打破属性值上下文。
  • 4、对 URL 参数部分进行完全的 URI 编码,如果在 JavaScript 中,使用 encodeURIComponent 函数

Vue 的 SSR 或 CSR 是否存在这个问题?

CSR:不存在,Vue 使用 setAttribute 天然避免此问题

SSR:Vue 只会对 URL 进行 html escape,但是不会对完整 URL 的进行 encodeURI,也不会对 URL 的参数部分进行 encodeURIComponent,需要我们自行完成。

案例三【用户数据作为 URL 的查询参数部分】:

<a href="https://api.foo.com?q={ userData }"></a>

如上代码所示,如果 userData 中包含诸如双引号(") ,或者 URL 保留字符【 ;,/?:@&=+$ 】以及 # 号等,就很容跳出属性值上下文或者造成错误的 url。

这时使用 encodeURI 是不行的,因为 encodeURI 不会对 URL 保留字符进行编码。

防御方式:

  • 我们需要使用另外一个函数,即 :encodeURIComponent() 函数,该函数会对 URL保留字符以及 # 号进行编码。
  • 对 URI 编码后的内容再进行一次 html escape ,以便将引号 "' 等内容进行转义,防止打破属性值上下文。

Vue 的 SSR 或 CSR 是否存在这个问题?

CSR:不存在,Vue 使用 setAttribute 天然避免此问题

SSR:存在,Vue 没有对其进行 encodeURIComponent,需要我们自行完成。

4、CSS

案例一【点击劫持】:

<a href="{ userData1 }" style="{ userData2 }"></a>

如上 a 标签所示,其 href 属性值与 style 样式完全由用户提供的数据决定,因此攻击者可以将该 a 标签定位到页面的任何位置,所谓点击劫持,就是通过 css 让标签完全透明,以至于用户看不到标签的存在,接着将标签定位到用户可能点击的位置,例如“登录”链接。当用户点击“登录”按钮时,实际点击的则是这个 a 标签,由于 href 属性也是一个完全由用户数据控制的,因此攻击者可以提供一个合法的 url 地址,例如: attacker.com/login ,这个页面看上去与真实网站的登录页面一模一样,用户在这个页面输入账号和密码,此时攻击者就完成了对受害者账号密码的盗用。

防御方式:

  • 永远避免使用用户提供的数据完全控制元素的样式
  • 可以使用用户数据指定特定的 css 属性值:

    • <a href="{ userData1 }" style="color: { userData2 }"></a>



Vue 的 SSR 或 CSR 是否存在这个问题?

对于点击劫持 SSR 和 CSR 都存在,如下代码会导致同样的问题:

<template>
	<a :href="userData1" :style="userData2"></a>
</template>

但这是 Vue 无法为我们避免的,因此我们不应该使用用户数据定义完整的 style 属性值,而是谨慎的使用用户数据定义部分 css 属性的值。

对于非点击劫持类的 css 相关的攻击,Vue 是不存在的,例如恶意的用户数据跳出 style 属性上下文等。

案例二【基于URL的属性值】:

css 中有很多属性,其属性值是基于 url 的,例如:

/* associated properties */
background-image: url("https://mdn.mozillademos.org/files/16761/star.gif");
list-style-image: url('../images/bullet.jpg');
content: url("pdficon.jpg");
cursor: url(mycursor.cur);
border-image-source: url(/media/diamonds.png);
src: url('fantasticfont.woff');
offset-path: url(#path);
mask-image: url("masks.svg#mask1");
/* Properties with fallbacks */
cursor: url(pointer.cur), pointer;
/* Associated short-hand properties */
background: url('https://mdn.mozillademos.org/files/16761/star.gif') bottom right repeat-x blue;
border-image: url("/media/diamonds.png") 30 fill / 30px / 30px space;
/* As a parameter in another CSS function */
background-image: cross-fade(20% url(first.png), url(second.png));
mask-image: image(url(mask.png), skyblue, linear-gradient(rgba(0, 0, 0, 1.0), transparent);
/* as part of a non-shorthand multiple value */
content: url(star.svg) url(star.svg) url(star.svg) url(star.svg) url(star.svg); 
/* at-rules */
@document url("https://www.example.com/") { ... }  
@import url("https://www.example.com/style.css");
@namespace url(http://www.w3.org/1999/xhtml);

是不是感觉吓了一跳,url() 函数还可以接受 data URI,如:url(data:image/png;base64,iRxVB0…); 。

因此如果有如下模板:

background-image: url({ userData });

如果 userData 的内容为:

userData = '"javascript:alert(document.cookie)"'

那么最终生成的 css 代码如下:

background-image: url("javascript:alert(document.cookie)");

这在旧版本的浏览器中会产生注入问题,新版本的浏览器已经可以自动避免此问题。

防御方式:

  • 严格校验协议,采用协议白名单的方式避免 javascript: 协议。
  • 保证 URL 语义的情况下对其进行 encode URI Component 编码

Vue 的 SSR 或 CSR 是否存在这个问题?

CSR:Vue 内部对 style 采用 setProperty 设置 CSS 属性,不存在跳出上下文的问题,只需要校验协议白名单即可。

SSR:Vue只对 css 属性值进行 html escape,如果是 URL 那么 Vue 没有对 URL 参数及以后部分采用 encodeURIComponent,这部分需要我们自行完成。

5、JavaScript 脚本中的用户数据

案例:

模板动态渲染 JavaScript 内容,如:

<script>const v = { userData }</script>
// 或者
<div onclick="{ userData }"></div>

恶意的用户可以很容易的通过指定 userData 达到注入的目的,例如 userData 的内容为:

userData = '"";</script><script>alert(document.cookie)</script><script>'