富文本编辑器是一个常见的业务场景,一般来说,通过富文本编辑器编辑的内容最终也会 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 mainimport ( "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 并没有本质的区别,主要就是基于标签和属性的过滤,可以基于自己的技术场景去选择。不过记得一点是两种方案过滤时机的选择。