长亭百川云 - 文章详情

DevKit系列1 - 可靠的 HTML 富文本过滤器

漕河泾小黑屋

32

2024-07-13

0x00 前言

许久以前接到一个需求,实现一个 HTML 富文本过滤的基础库。这个需求在其它语言实现中有许多久经考验的开源库,比如 NodeJS 有 DOMPurify ,但在 Go 中却异常尴尬,没有一个合适的、久经考验的 HTML 富文本过滤库。即使运气好找到了一个,也很难保证这个库是安全可靠的。思来想去,还是决定自己做一套性能扛得住、安全可靠的 Go 语言实现。

0x01 原理

咱们的目标是做一套性能扛得住、安全可靠的 Go 语言实现,其核心关键词是性能安全

  • 出于安全考虑,这里不能轻易地使用第三方的 DOM 解析库(毕竟也不知道靠不靠谱),最为稳妥的办法是做一个 HTML 的最小语义支持,不管输入如何,这个库只支持它认为正常的HTML 语法。

  • 要满足性能需求,算法复杂度不宜太高,最好是线性扫描

所以最终决定使用 DFA(确定有限状态自动机) 从 0 构建一个 HTML 解析器。

提到 DFA 有些同学可能会一头雾水,但提到正则表达式大家可能会相对熟悉一点。一个正则表达式,可能是一个 DFA,也有可能是一个 NFA(非确定有限状态自动机)。比如

  • a*ab 这个正则表达式是一个 NFA

  • a+b 这个正则表达式则是一个 DFA

很明显,上面两个正则表达式是等价的,NFA 是可以和 DFA 互转的。

实现具体的 DFA 之前,我们需要先把整个状态机的实现勾勒出来,避免写代码的时候一头雾水。因此,我们按照设想的 “HTML 的最小语义支持”,画了下面这张状态图。

实际上 ETAG_END、TAG_END、NORMAL 是同一种状态,但为了实现方便,这里拆成了三种状态


0x02 实现

安全标签+安全属性

状态机画出来后,还需要总结出所有的安全标签+安全属性。安全标签这个概念比较好理解,类似与 <script> 这种可以造成 XSS 的标签,肯定不属于安全标签。类似的,onerror 这类属性,肯定也不属于安全属性。最终我们梳理出了这么一份安全标签+安全属性列表 (https://github.com/SYM01/htmlsanitizer/blob/ba260fbd09d62fb76ca02de647ec9e3aa4e3c545/tags.go#L134-L222)。

在白名单中,我们还将安全属性的定义划分为两种,一种是普通属性(Attr),如 colspanrowspan 这种人畜无害的属性;另外一种则是 URL 属性(URLAttr),如 srchref 这种在特定场景下有危害的场景。

一个实际的例子如下,这种情况下是有 XSS 问题的。

<a href="jav&#x09;ascript:alert('XSS');">Click Me!</a>

因此,这类 URL 属性(URLAttr)需要进行特殊处理。目前的默认的处理机制为,对这些 URL 进行解析,一旦解析失败,或者解析后的 schema 不为 http / https ,则认为存在问题,不予输出。

状态机实现

安全标签+安全属性列表、状态机就绪后,实现上就相对粗暴简单了,使用一个 swtich 语句完成状态间的转移即可,如下面的代码片段所示:

func (w *writer) Write(p []byte) (n int, err error) { // reset data w.data = p w.off = 0 for err == nil && w.off < len(p) { switch w.state { case sNORMAL: err = w.sNORMAL() case sLTSIGN: err = w.sLTSIGN() case sTAGNAME: err = w.sTAGNAME() // ... case sATTRNAME: err = w.sATTRNAME() // ... case sATTRVAL: err = w.sATTRVAL() case sVALSPACE: err = w.sVALSPACE() case sATTRQVAL: err = w.sATTRQVAL() // ... default: panic("unknown state") } } n = w.off return }

这里有3个状态的处理非常重要,分别为 w.sTAGNAME()、 w.sATTRVAL() 和 w.sATTRQVAL() 。

w.sTAGNAME()

用于处理标签名,比如 <a> 这种。在完成一个标签的识别后,需要校验这个标签名是否为安全标签,否则干掉。

w.sATTRVAL() 和 w.sATTRQVAL()

这两个状态处理函数的作用非常类似,唯一的区别是前者用于处理不带引号的属性值,如<a href=xxx>这种。因此将两者放在一起讨论。

之所以说这两个状态处理函数非常重要,是因为属性值可能是一个 URL ,如上文讨论的 case。因此,这两个状态处理函数必须校验当前属性是否为 URL 属性(URLAttr),如是,则必须调用 URL 校验函数,对该 URL 进行校验,校验失败则不输出。

0x03 总结

该 HTML 富文本过滤器的核心是实现一个最小语义版本,通过一个最小化的语法支持,抹平不同浏览器的实现差异,达到安全的目的。

目前基于 DFA 实现的 HTML 富文本过滤器已经发布 v1.0.1 版本,测试用例覆盖度也足够满足生产环境的使用要求,欢迎找茬、打脸、绕过。

项目地址:https://github.com/SYM01/htmlsanitizer

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2