功能说明
这段代码演示如何安全地把用户生成内容嵌入 HTML。作者名、正文和链接都来自不可信输入:作者名包含 HTML 标签,正文包含 <script>,链接使用 javascript: 协议。html/template 会根据每个值所处的上下文自动转义,阻止脚本执行。
它适合评论、个人简介、工单描述等需要展示用户文本的页面。与手工拼接字符串相比,模板引擎还能识别属性、URL、CSS 和 JavaScript 上下文,降低遗漏转义的风险。
关键代码
template.Must(template.New("comment").Parse(...)) 编译模板,模板中的 {{.Author}}、{{.Body}} 和 href="{{.Link}}" 分别位于文本和 URL 属性上下文。
执行后,正文中的 <、> 和引号被转成 HTML 实体;javascript: 链接被替换为 #ZgotmplZ,这是 html/template 对危险协议的标记。代码用 strings.Contains 断言渲染结果中不再出现原始 <script>。
使用注意
绝对不能把不可信内容包装成 template.HTML、template.JS 或 template.URL,否则会绕过转义。模板本身可以包含受信任的静态 HTML,但用户数据必须通过 {{...}} 插值。
html/template 不能替代输入校验:例如用户仍可在正文中放入钓鱼链接文本。富文本场景应使用白名单清洗器,而不是简单禁用转义。属性值必须加引号,未加引号的属性会改变上下文判断。text/template 不进行 HTML 转义,不能用于网页输出。
验证方式
运行示例会打印渲染后的 <article>,其中作者标签变成实体,正文显示为文本而不是脚本,链接变成 #ZgotmplZ。三个 strings.Contains 断言分别确认没有原始脚本标签、存在转义后的文本、危险 URL 被拦截。可以再加入 data: 图片 URL 和事件属性,观察模板的上下文防护。
代码示例
package main
import (
"bytes"
"fmt"
"html/template"
"strings"
)
type Comment struct {
Author string
Body string
Link string
}
func main() {
page := template.Must(template.New("comment").Parse(
`<article><h2>{{.Author}}</h2><p>{{.Body}}</p><a href="{{.Link}}">profile</a></article>`,
))
comment := Comment{
Author: "<b>Mallory</b>",
Body: `<script>alert("x")</script>`,
Link: "javascript:alert(1)",
}
var output bytes.Buffer
if err := page.Execute(&output, comment); err != nil {
panic(err)
}
rendered := output.String()
fmt.Println(rendered)
if strings.Contains(rendered, "<script>") {
panic("script tag was not escaped")
}
if !strings.Contains(rendered, "<script>") {
panic("expected escaped body text")
}
if !strings.Contains(rendered, "#ZgotmplZ") {
panic("expected dangerous URL to be blocked")
}
}