富文本不能直接用html.EscapeString,因其仅转义5个字符,无法防御事件属性等XSS;必须用bluemonday白名单过滤后转template.HTML,且href/src等属性需用url.URL类型或template.URL包装,文件读取内容须手动html.EscapeString。

富文本为什么不能直接用 html.EscapeString
因为 html.EscapeString 只转义 、<code>>、&、"、' 这 5 个字符,对 <img src=x onerror=alert(1)> 这类带事件属性的标签完全无效。它只适合纯文本插入 <p>{text}</p> 场景,不是富文本解决方案。
常见错误现象:用户提交了 <a href="javascript:alert(1)">点我</a>,你调了 html.EscapeString 后传进模板,结果 href 属性里还是能执行 JS——因为协议没被拦截,转义也没覆盖 javascript: 这种上下文。
- 富文本必须走白名单过滤,不是“删标签”或“正则替换”
- 不要自己写
strings.ReplaceAll或正则去“消毒”,这会漏掉onerror、data:text/html等隐蔽入口 -
bluemonday.UGCPolicy()是当前最稳妥的选择,它默认允许p、strong、img(带src、alt)、a(仅限http://、https://协议)等安全子集
过滤后怎么安全传入 html/template
过滤完的字符串仍是普通 string,不能直接塞进模板——必须显式转成 template.HTML 类型,否则 html/template 仍会把它当普通文本再转义一遍,导致 <p>Hello</p> 显示为源码。
关键点在于:这个转换动作是“信任声明”,不是“过滤动作”。上游没过滤干净,这里包一层就等于开后门。
立即学习“go语言免费学习笔记(深入)”;
- 先用
bluemonday.UGCPolicy().SanitizeBytes([]byte(raw))得到清洗后的字节 - 转成
string后,立刻包装为template.HTML(cleaned) - 模板里写
{{.SafeContent}},不加| safeHTML(该函数 Go 1.12+ 已弃用) - 禁止在 handler 层返回
map[string]any{"Content": template.HTML(userInput)}—— 这等于跳过过滤
href、src 等属性里插用户数据依然危险
html/template 对属性值做的是上下文转义(比如把 " 变成 "),但它不校验协议合法性。所以 href="{{.URL}}" 中填 javascript:alert(1),浏览器照样执行。
正确做法不是靠模板转义,而是从源头控制类型:
- 字段类型应为
url.URL,而不是string;构造时用url.Parse校验,失败则拒绝 - 若必须用 string,传入前用
template.URL包装:template.URL("https://example.com"),模板中写href="{{.URL}}" -
src同理,图片地址优先走template.URL,且后端应限制协议为https?和data:image/ - 绝对不要写
href={{.URL}}(缺引号),这会让模板无法识别上下文,转义彻底失效
读文件内容插入模板前必须手动转义
像 os.ReadFile("article.md") 这种读出来的内容,哪怕路径是硬编码、文件是你自己写的,只要内容可能被用户间接影响(比如 CMS 文章、日志归档页),就必须过 html.EscapeString。
html/template 不会自动帮你转义外部读入的字符串——它只对 {{.Field}} 插值做转义,前提是 .Field 是 string 类型;一旦你提前包成 template.HTML,就绕过了所有防护。
- 读取后立即调
html.EscapeString(string(b)) - 把结果作为普通
string字段传入模板数据结构 - 模板中仍用
{{.Content}},让html/template再做一次上下文转义(这是安全冗余,不是重复劳动) - 别用
template.HTMLEscapeString替代——它和html.EscapeString行为一致,但语义更模糊,易混淆
template.HTML 包装 → 模板直插。中间跳过任意一环,XSS 就可能落地。最容易被忽略的是属性上下文(href、src)和文件读取场景——这两处没有自动防护,全靠开发者主动加类型约束或手动转义。



















