应使用 bluemonday 过滤富文本并配合 html/template 渲染,禁用正则清洗和 html.EscapeString 预处理;template.HTML 仅表示信任,不提供过滤;输入校验不能替代输出编码。

别在输入阶段做 HTML 标签清洗,也别用正则删 onerror 或 javascript: —— 这类操作既多余又危险,还容易漏掉大小写混用、URL 编码、嵌套属性等真实绕过方式。
html.EscapeString 只适用于纯文本插入 HTML 内容区
它只转义 <、>、"、'、& 这 5 个字符,对 javascript:alert(1)、onmousemove=alert(1)//、data:text/html;base64,... 完全无效。
仅当用户输入将被直接放入 HTML 元素内容中(非属性、非 script、非 style)时才安全,例如:
<p>{{.UserComment}}</p>
- ✅ 正确:用
html/template渲染,{{.UserComment}}自动转义 - ❌ 错误:把
html.EscapeString(input)结果再塞进href或onclick属性里 - ❌ 错误:先调用
html.EscapeString再传给template.HTML包装——多此一举且破坏上下文感知
template.HTML 是信任开关,不是过滤开关
一旦你写 template.HTML(userInput),就等于告诉模板引擎:“这串 HTML 我已确认安全”,引擎会跳过所有转义逻辑原样输出。
立即学习“go语言免费学习笔记(深入)”;
常见翻车场景:
- 从 URL 参数取值:
r.URL.Query().Get("q"),未经 bluemonday 过滤就template.HTML(q) - 数据库读出的富文本字段,没走净化直接
{{.Content | safeHTML}} - 前端富文本编辑器提交的 HTML,后端只做
strings.ReplaceAll就标记为安全
真正该做的:把过滤逻辑提到 handler 层,用 bluemonday.UGCPolicy().Sanitize() 处理完,再包成 template.HTML 传入模板。
富文本必须用 bluemonday,别信正则或自定义白名单
正则无法解析 HTML 结构,处理不了编码混淆(jaVaScRiPt:)、注释干扰(onerror="alert(1)<!-- -->")、嵌套标签等真实攻击变体。
bluemonday 基于 HTML 解析树工作,先 parse 成 DOM,再按策略删节点和属性,可靠得多:
import "github.com/microcosm-cc/bluemonday" <p>p := bluemonday.UGCPolicy() clean := p.Sanitize(userInput) // 保留 <strong>、<ul> 等常用标签
- ✅ 用
UGCPolicy():支持常见富文本语义,不过度阉割 - ❌ 别用
StrictPolicy():会干掉所有标签,包括<b>和<em> - ⚠️ 过滤后仍需走
html/template渲染,不能fmt.Fprintf(w, "%s", clean)裸输出
输入校验只做最小化约束,不替代输出编码
输入层该做的事很有限:限制长度、拒绝空字节 \x00、拦截明显恶意前缀(如 <script 开头)、校验邮箱/手机号格式。但它不是 XSS 防线主体。
- ✅ 对表单字段加结构体标签验证:
type Form struct { Name string `validate:"alphanum,required,max=50"` } - ✅ 拒绝含
%00、%0a、../的路径类参数 - ❌ 不要用正则全局删
<.*?>—— 会破坏合法 HTML 实体(如) - ❌ 不要在中间件里统一调用
html.EscapeString后再存库 —— 输出时还要再转义一次,反而可能双重编码
最易被忽略的一点:所有用户可控数据,只要最终要进 HTML 上下文,就必须依赖 html/template 的上下文感知转义能力;任何试图“提前消毒”的做法,都会干扰它本应正确工作的机制。


















