登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go html/template 如何区分安全模板与纯文本模板

来源:17golang原创

时间:2026-09-12 18:39:56 427浏览 收藏

我第一次把 Go 模板从邮件正文迁到网页页面时,最容易混淆的就是 html/templatetext/template:两者都能解析 {{.Name}},但输出目标不同,安全边界也不同。结论很简单——要生成 HTML,就选 html/template;要生成纯文本,再选 text/template。不要因为接口相似,就把纯文本模板当成 HTML 模板使用。

普通数据交给 html/template 时,包会根据它所在的 HTML、属性、JavaScript、CSS 或 URI 上下文自动转义;text/template 不承担这层 HTML 安全职责。只有确定来自可信来源的标记片段,才考虑 template.HTML 等安全类型。
要点速览
  • HTML 页面、属性和脚本中的动态值,默认使用 html/template
  • 纯文本邮件、日志或配置输出使用 text/template,但它不会替你防止 HTML 注入。
  • template.HTML 是“已确认安全”的类型,不是把用户输入变安全的函数。

先按输出格式选择模板包

text/template 的职责是把数据渲染成文本,输出可以是 Markdown、邮件、SQL 片段或配置文件;它不知道这段文本最后会不会被浏览器当成 HTML。官方文档也明确建议:如果输出是 HTML,应改用 html/template

html/template 复用了 text/template 的大部分接口,但会把动作放回 HTML 结构中分析。例如同一个值出现在标签正文和 href 属性里,所需的转义规则并不一样。这个选择应在包导入处完成,而不是等输出异常后再补一个手工替换。

普通字符串为什么在两个包里表现不同

假设数据中有一段 。用 text/template 渲染时,它会原样出现在文本结果里;如果这段结果又被浏览器当作 HTML 解析,就可能变成代码注入入口。用 html/template 时,普通字符串会被编码为文本,尖括号等特殊字符不再改变页面结构。

更关键的是,html/template 不只做一次固定的 HTML 替换,而是按上下文处理。下面的写法用别名区分两个包,示例输出中的转义结果表示它们的安全职责差异:

package main

import (
    htmltmpl "html/template"
    "os"
    texttmpl "text/template"
)

func main() {
    data := struct{ Value string }{Value: ""}

    // 纯文本模板只负责替换动作,不判断结果会被什么解析器消费。
    plain := texttmpl.Must(texttmpl.New("plain").Parse("{{.Value}}"))
    _ = plain.Execute(os.Stdout, data)

    // HTML 模板把普通字符串当作不可信文本,并按所在上下文添加转义。
    page := htmltmpl.Must(htmltmpl.New("page").Parse("

{{.Value}}

")) _ = page.Execute(os.Stdout, data) }

在真实页面中还要留意属性和脚本上下文,例如 与标签正文需要不同处理。不要先用 html.EscapeString 把所有值处理一遍再塞进模板;这会让上下文判断失去意义,也容易造成重复转义。

html/template 将普通字符串放入 HTML 正文、href 属性和 JavaScript 字符串时的静态上下文关系示意图
图1:html/template 的静态上下文关系示意图;同一个普通字符串进入不同 HTML 上下文时,会经过对应的安全转义。

纯文本模板并不等于不安全

text/template 本身没有问题,关键是它的输出协议。生成纯文本邮件、命令行帮助或配置文件时,保留原字符通常正是需求;但如果模板结果随后会进入浏览器、HTML 邮件或某个会执行标记的消费者,就应该在边界处改用适合该格式的编码策略。

我通常把判断写成一句工程规则:模板包跟着最终解释器走。浏览器解释 HTML,就让 html/template 负责上下文转义;纯文本阅读器不解释标签,才使用 text/template。不要只看文件名是 .tmpl 还是 .tpl,也不要用“当前输入看起来没有尖括号”来证明选择正确。

template.HTML 不是用户输入的快捷通行证

html/template 默认把执行数据当作普通文本。确实有些场景需要输出由程序或可信内容系统生成的 HTML 片段,这时可以使用 template.HTML 等带语义的安全类型,让它不要被再次当成普通文本转义。但类型转换只是在代码中声明“这段内容已经经过安全审查”,不会替你清理输入。

package view

import (
    "html/template"
    "io"
)

type Data struct {
    Title string
    Body  template.HTML
}

func Render(w io.Writer, title string, trustedBody template.HTML) error {
    const page = "

{{.Title}}

{{.Body}}
" tmpl, err := template.New("page").Parse(page) if err != nil { // 模板是受信任代码的一部分,解析失败应直接返回并修复模板。 return err } // Title 仍是普通 string;只有已经过业务审核的 Body 才使用安全类型。 return tmpl.Execute(w, Data{Title: title, Body: trustedBody}) }

这里的 trustedBody 不应直接来自表单、评论、URL 参数或数据库中未经处理的用户内容。更稳妥的做法是让富文本清洗器先输出允许的有限标签,或者把内容设计成结构化数据,由模板负责生成固定 HTML。能不用安全类型时,优先不用。

受信任模板作者、Execute 数据、普通 string、template.HTML、html/template 与浏览器输出之间的静态信任边界示意图
图2:模板安全边界示意图;模板作者应受信任,Execute 数据默认按普通文本处理,安全类型只能来自已确认的可信片段。

迁移时只检查四个边界

  1. 检查导入包。网页渲染的文件统一确认导入的是 html/template,不要只因为旧代码能编译就保留 text/template
  2. 检查数据去向。列出每个动态值最终由谁解释:浏览器、邮件客户端、日志查看器还是配置加载器。
  3. 检查安全类型转换。搜索 template.HTMLtemplate.JStemplate.URL 等转换,逐个标注来源和审查理由。
  4. 检查恶意字符测试。至少覆盖尖括号、引号、反斜杠和属性值中的 URI;测试目标是页面结构不被输入改变,而不是只比较字符串是否“好看”。

模板作者本身也属于信任边界。html/template 的模型是模板作者可信、执行数据不一定可信;如果模板文件来自不可信用户,不能把换包当成沙箱。最后再记一遍:选择包解决的是输出格式与默认转义,不能替代输入校验、富文本清洗、权限控制和 CSP 等其他措施。

常见问题

text/template 能不能渲染 HTML?

能生成 HTML 字符串,但不会自动按 HTML 上下文转义。只要结果会交给浏览器解析,优先改用 html/template

html/template 会不会把所有 HTML 都转义掉?

普通字符串会按上下文安全编码;明确使用 template.HTML 等安全类型的值可能保留标记。因此安全类型的来源必须可解释、可审查。

参考资料:https://pkg.go.dev/html/templatehttps://pkg.go.dev/text/templatehttps://go.dev/doc/

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>