阅读 584

给 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 实体:

  • & → &
  • < → <
  • → >

  • " → "
  • ' → '
  • / → /

这样,转义后的 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="" onclick=alert(document.cookie)></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:alert(document.cookie)></a>
复制代码

这仍然是问题代码。

防御方式:

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

const allowed = ['http', 'https']

const valid = isValid(userData, allowed)
// ...
复制代码

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

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 属性值:

  • 把所有非数字和字母之外的所有字符转移成 \HH 的形式,其中 HH 代表 16 进制值。

  • CSS 中转义十六进制的注意事项请阅读:www.w3.org/Internation…

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>'
复制代码

那么生成的 html 则为:

<script>const v = "";</script><script>alert(document.cookie)</script><script></script>
复制代码

很显然产生了注入。

防御方式:

  • 在将 userData 插入之前,将所有非数字和字母的字符转义为 \xHH 或 \uHHHH 或 \x{H...H} 的格式,其中 HH 为 16 进制值。

  • 需要注意的是,即使进行转义,有些代码仍然是不安全的,例如:

  • setTimeout('{ userData}') setInterval('{ userData }') eval('{ userData }') 应避免这样做。

  • 这是反模式的做法:,请永远不要这么做,如果一定要这么做请确保 userData 经过以下两个方法处理:

  • 方法一:使用 github.com/yahoo/seria… 对 userData 进行编码,它会对 html 相关的字符进行编码处理,如: '' 。

  • 方法二:将 userData 进行 html 转义后放到一个隐藏的 html 标签中,然后使用 innerHTML 获取其内容并使用 JSON.parse 获得数据:

  • { htmlEscape(userData) }
    const data = JSON.parse(document.getElementById('data-box').innerHTML)

Vue 是否存在这个问题?

无论是 SSR 还是 CSR:Vue 的模板可以渲染

6、其他重要事项

cookie 采用 httpOnly

将敏感的以及用来保持服务器会话的 cookie 设置为 httponly,它将禁止 JavaScript 访问。

采用 Content Security Policy (CSP)

简单的说,CSP 浏览器端指定资源白名单的方式,推荐阅读文章:

content-security-policy.com/

DOM 型 XSS【Dom based XSS】

虽然 DOM 型 XSS 是纯客户端的安全漏洞,但仍要区分在客户端执行的代码是否由服务端渲染所提供的,例如:

const div = document.createElement('div')
div.innerHTML = { userData }
复制代码

如果这段代码是在服务端拼接后发送至客户端的,那么所有 DOM 型攻击的防御方式都需要遵照:存储型和反射型 XSS 的防御方式。或参考:github.com/OWASP/Cheat… 中的描述来完成。

而对于 js 脚本的内容与服务端无关的,即所有 js 代码都不是由服务端拼接并提供的,那么其防御方式我们已经在讲解存储型和反射型 XSS 的防御方式以及 Vue 的 SSR 和 CSR 中是否存在此问题时讲解过了。

DOM 型 XSS 是纯客户端的安全漏洞,当浏览器渲染 html 页面 (包括相关的 JS CSS 资源) 时,会根据不同输入识别不同的渲染上下文,并在不同的上下文采用不同的渲染规则,这些上下文包括但不限于:

  • 普通 HTML 标签
  • html 的属性
  • 属性值 URL 的 html attribute 或 css property
  • script 标签
  • style 标签
  • 其他...

通常我们只讨论 HTML、HTML attribute、 URL 和 CSS 这四个子上下文,因为这四个子上下文是 JavaScript 可以通过代码到达的,例如:

const div = document.createElement('div')
div.innerHTML = userData
document.body.appendChild(div)


复制代码

如上代码在 JavaScript 执行上下文中通过标签元素的 innerHTML 属性进入到了 HTML 子上下文。

总结

本文尽量涵盖大部分 XSS 相关的安全知识,但很难涵盖完整, 甚至笔者在整理的过程中已经选择性的抛弃了一些知识,但主要的内容基本都有体现,安全问题是一个需要根据不同场景 (如不同上下文以及不同上下文内容的渲染方式) 采用不同防御方式的问题,因此我们要学习的根本内容是去了解:不同的上下文中所拥有的能力,以及这些能力如果由用户控制会产生哪些影响。只要我们掌握了本质才能做到更好的防御,而不是死记硬背。

不同上下文的 escape 实现可以参考

github.com/ESAPI/owasp…

github.com/chrisisbeef…

References

文章分类
前端
文章标签