富文本编辑器是一个常见的业务场景,一般来说,通过富文本编辑器编辑的内容最终也会 html 的形式来进行渲染,比如 VUE,一般就会使用 v-html
来承载富文本编辑的内容。因为文本内容需要通过 html 来进行渲染,那么显然普通的编码转义不适合这种场景了,因为这样最终的呈现的效果就不是我们想要的了。针对于这种场景,显然过滤是唯一的解决方案了,不过过滤其实可以在后端和前端都是可以做的,后端做的话,一般是在数据存储在数据库之前。前端做的话,则是在数据最终在页面渲染之前做过滤。
前端的过滤方案,可以尝试使用开源的 [js-xss](https://github.com/leizongmin/js-xss)
。先介绍一下这个库的使用方法,这个库可以在 nodejs 中使用,同样也可以在浏览中直接引入使用。
// nodejs 中使用
var xss = require("xss");
var html = xss('<script>alert("xss");</script>');
console.log(html);
// 浏览器中使用
<script src="https://rawgit.com/leizongmin/js-xss/master/dist/xss.js"></script>
<script>
// apply function filterXSS in the same way
var html = filterXSS('<script>alert("xss");</scr' + "ipt>");
alert(html);
</script>
一般在 vue 的项目中,通过 webpack 也可以直接通过 CommonJS 的方式引入,与 nodejs 的引入方式基本一致。值得注意的一个问题是,默认情况下会去禁用 style 属性,这样会导致富文本的样式展示异常,需要禁用 css 过滤或者使用白名单的方式来进行过滤。
const xssFilter = new xss.FilterXSS({
css: false
});
html = xssFilter.process('<script>alert("xss");</script>');
const xssFilter = new xss.FilterXSS({
css: {
whiteList: {
position: /^fixed|relative$/,
top: true,
left: true,
},
},
});
html = xssFilter.process('<script>alert("xss");</script>');
其实 js-xss 的原理并不是很复杂,如果去扒一下源码,原理其实主要就是实现标签和属性的白名单过滤,这样的方案简单有效。因为默认配置了大部分标签以及属性的白名单方案,所以一般可以做到开箱即用,当然如果有定制化的需求需要进一步定制化函数。
function getDefaultWhiteList() {
return {
a: ["target", "href", "title"],
abbr: ["title"],
address: [],
area: ["shape", "coords", "href", "alt"],
article: [],
aside: [],
audio: [
"autoplay",
"controls",
"crossorigin",
"loop",
"muted",
"preload",
"src",
],
b: [],
bdi: ["dir"],
bdo: ["dir"],
big: [],
blockquote: ["cite"],
br: [],
caption: [],
center: [],
cite: [],
code: [],
col: ["align", "valign", "span", "width"],
colgroup: ["align", "valign", "span", "width"],
dd: [],
del: ["datetime"],
details: ["open"],
div: [],
dl: [],
dt: [],
em: [],
figcaption: [],
figure: [],
font: ["color", "size", "face"],
footer: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
header: [],
hr: [],
i: [],
img: ["src", "alt", "title", "width", "height"],
ins: ["datetime"],
li: [],
mark: [],
nav: [],
ol: [],
p: [],
pre: [],
s: [],
section: [],
small: [],
span: [],
sub: [],
summary: [],
sup: [],
strong: [],
strike: [],
table: ["width", "border", "align", "valign"],
tbody: ["align", "valign"],
td: ["width", "rowspan", "colspan", "align", "valign"],
tfoot: ["align", "valign"],
th: ["width", "rowspan", "colspan", "align", "valign"],
thead: ["align", "valign"],
tr: ["rowspan", "align", "valign"],
tt: [],
u: [],
ul: [],
video: [
"autoplay",
"controls",
"crossorigin",
"loop",
"muted",
"playsinline",
"poster",
"preload",
"src",
"height",
"width",
],
};
}
另外前端过滤的时机一般是选择数据在页面渲染之前。在 vue 中,选择在 created()
做过滤即可。不过在 JS 中有一种绕过过滤的方案,就是在过滤函数之前让 JS 报错,那么这样过滤函数就不会执行了,从而导致绕过。
这么看来,在数据储存之前,后端做过滤也不失为一个稳妥的方案。因为公司是以 golang 为主的技术栈,就讨论一下 golang 方面的技术方案。bluemonday 是一款 golang 的 HTML 过滤器,相对于 js-xss 来说,这个库的可定制性更高。
基于默认的过滤策略:
Hello <STYLE>.XSS{background-image:url("javascript:alert('XSS')");}</STYLE><A CLASS=XSS></A>World
会被过滤为
Hello World
而对于:
<a href="http://www.google.com/">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
大部分内容不会变化,只是给 a 标签增加了一个 rel 属性,更安全。
<a href="http://www.google.com/" rel="nofollow">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
默认的策略使用 bluemonday 非常方便:
package main
import (
"fmt"
"github.com/microcosm-cc/bluemonday"
)
func main() {
p := bluemonday.UGCPolicy()
html := p.Sanitize("<a onblur="alert(secret)" href="http://www.google.com">Google</a>")
fmt.Println(html)
}
另外定制性真的特别强大,语义性好,傻瓜式入门,可以便捷地自定义过滤策略。
p := bluemonday.NewPolicy()
// 标签白名单
p.AllowElements("b", "strong")
// 正则表达式白名单
p.AllowElementMatch(regex.MustCompile("^my-element-"))
其实从原理上来说,bluemonday 与 js-xss 并没有本质的区别,主要就是基于标签和属性的过滤,可以基于自己的技术场景去选择。不过记得一点是两种方案过滤时机的选择。