必须对读取的文件内容调用html.EscapeString转义后再传入html/template,因其不自动转义os.ReadFile等读出的字符串;若为富文本则需先用bluemonday白名单过滤再转template.HTML。

直接读取文件内容后插入 HTML 模板,不转义就等于把 XSS 入口焊死在页面上——哪怕文件是自己写的、路径是硬编码的,只要内容可能被用户间接影响(比如日志归档、配置模板、CMS 文章源),就必须做 HTML 转义。
读文件后用 html.EscapeString 再传给 html/template
Go 的 html/template 不会自动帮你转义从 ioutil.ReadFile 或 os.ReadFile 读出来的字符串。它只对模板里 {{.Field}} 插值做上下文感知转义,但前提是这个 .Field 是普通 string 类型;一旦你提前把它包成 template.HTML,就彻底绕过防护了。
正确链路是:
- 用
os.ReadFile("content.txt")读出原始字节 - 转成
string后立刻调html.EscapeString(content) - 把结果作为
string字段传入模板数据结构,不要强转template.HTML - 模板中写
{{.Content}},让html/template再做一次最终上下文转义(安全冗余)
template.HTMLEscapeString 和 html.EscapeString 用哪个
两者行为一致,都只转义 、<code>>、&、"、' 这 5 个字符。区别在于包路径和使用场景:
立即学习“go语言免费学习笔记(深入)”;
-
html.EscapeString是标准库函数,可 anywhere 调用,适合文件读取、日志拼接、API 响应体构造等非模板场景 -
template.HTMLEscapeString是html/template包内函数,语义更明确,但仅限模板逻辑内部使用;实际项目中混用无害,但建议统一用html.EscapeString避免依赖错位 - 别用
strings.ReplaceAll手动替换\n→<br>后再传进模板——这会导致<br>被二次转义成<br>
富文本文件(如 Markdown、HTML 片段)不能只靠转义
如果文件本身含合法 HTML 标签(例如 CMS 导出的 article.html),html.EscapeString 会把所有标签变成纯文本,失去语义。这时必须分两步走:
- 先用白名单过滤器(如
bluemonday)清洗:只保留p、strong、img等安全标签及src、alt等属性 - 再把清洗后的字符串显式转为
template.HTML(cleaned),并确保该字段类型就是template.HTML - 模板中直接写
{{.SafeContent}},不加任何 filter,不调safeHTML(已弃用) - 禁止对原始文件内容做正则“消毒”或
strings.Replace替换事件属性——这类操作无法覆盖onerror、style="background:url(javascript:...)"等隐蔽向量
为什么 href="{{.URL}}" 即使转义了还是危险
html/template 知道 href="" 是 URL 上下文,所以会对 " 和 & 做转义,但它不会拒绝 javascript: 协议或校验域名。如果文件里存的是 javascript:alert(1),转义后仍是合法 URL,浏览器照执行。
- 解决方案:读取后先解析为
url.URL,检查.Scheme是否在["http", "https", "mailto"]白名单内 - 或者用
template.URL类型包装:字段定义为URL template.URL,赋值前做协议校验,模板中写href="{{.URL}}" - 绝对不要在模板里写
href={{.URL}}(缺引号)——这会让html/template无法识别上下文,直接放弃转义
文件内容是否可信,不取决于路径是否固定,而取决于它的修改入口是否可控。一个被 CMS 后台编辑过的 /static/article.html,和一个用户上传的 bio.txt,安全边界是一样的——该转义就得转义,该过滤就得过滤,没有例外。



















