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><script>alert(document.cookie)</script></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)
// ...
或者使用开源的库做过滤,如 https://www.npmjs.com/package/@braintree/sanitize-url
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 地址,例如:http://attacker.com/login,这个页面看上去与真实网站的登录页面一模一样,用户在这个页面输入账号和密码,此时攻击者就完成了对受害者账号密码的盗用。
防御方式:
- 永远避免使用用户提供的数据完全控制元素的样式
- 可以使用用户数据指定特定的 css 属性值:
- <a href="{ userData1 }" style="color: { userData2 }"></a>
- 把所有非数字和字母之外的所有字符转移成 HH 的形式,其中 HH 代表 16 进制值。
- CSS 中转义十六进制的注意事项请阅读:https://www.w3.org/International/questions/qa-escapes#question
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 }')应避免这样做。
- 这是反模式的做法:<script>const v = { userData }</script>,请永远不要这么做,如果一定要这么做请确保 userData 经过以下两个方法处理:
- 方法一:使用 https://github.com/yahoo/serialize-javascript 对 userData 进行编码,它会对 html 相关的字符进行编码处理,如: '</script>' 。
- 方法二:将 userData 进行 html 转义后放到一个隐藏的 html 标签中,然后使用 innerHTML 获取其内容并使用 JSON.parse 获得数据:
- <div style="display: none;" id="data-box">{ htmlEscape(userData) }</div>const data = JSON.parse(document.getElementById('data-box').innerHTML)
Vue 是否存在这个问题?
无论是 SSR 还是 CSR:Vue 的模板可以渲染 <script> 标签,但 userData 不会导致跳出该 <script> 上下文,不过还是可以将用户数据作为代码执行的,因此它是不安全的,而且在 Vue 模板中渲染 <script> 标签就意味着组件产生了副作用,这是反模式。
6、其他重要事项
cookie 采用 httpOnly
将敏感的以及用来保持服务器会话的 cookie 设置为 httponly,它将禁止 JavaScript 访问。
采用 Content Security Policy (CSP)
简单的说,CSP 浏览器端指定资源白名单的方式,推荐阅读文章:
https://content-security-policy.com/
DOM 型 XSS【Dom based XSS】
虽然 DOM 型 XSS 是纯客户端的安全漏洞,但仍要区分在客户端执行的代码是否由服务端渲染所提供的,例如:
const div = document.createElement('div')
div.innerHTML = { userData }
如果这段代码是在服务端拼接后发送至客户端的,那么所有 DOM 型攻击的防御方式都需要遵照:存储型和反射型 XSS 的防御方式。或参考:https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.md 中的描述来完成。
而对于 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 实现可以参考
https://github.com/ESAPI/owasp-esapi-js
https://github.com/chrisisbeef/jquery-encoder/blob/master/src/jquery.jquery-encoder.js
References
- https://owasp.org/www-community/Types_of_Cross-Site_Scripting
- https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-6---sanitize-html-markup-with-a-library-designed-for-the-job'
- https://vuejs.org/v2/guide/security.html
- https://github.com/OWASP/owasp-java-encoder
- https://docs.huihoo.com/google-ctemplate/auto_escape.html
- https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.md