应使用template.html类型包装可信html内容,模板中仍写{{.content}};禁用|safe等不安全方式,url、js、css等需分别用template.url、template.js、template.css标注。

模板里 {{.Content}} 被自动转义成 <script> 怎么办</script>
html/template 默认对所有变量执行 HTML 转义,这是安全机制,不是 bug。当你传入 "<script>alert(1)</script>" 这类含 HTML 标签的字符串,{{.Content}} 会输出 <script>alert(1)</script>,浏览器不会执行脚本——这正是你该期待的行为。
但如果你确认这段内容是可信的(比如后台富文本经 bluemonday 过滤后),又想让它原样渲染为 HTML,就不能靠“关掉转义”这种粗暴方式,而要用官方认可的“类型标注”:
- 后端把内容包装成
template.HTML类型,再传给模板 - 模板中仍写 {{.Content}},但此时引擎识别出它是
template.HTML,跳过 HTML 转义,仅做上下文安全处理(如在属性中仍会编码引号) - 绝对不要用
| safe或{{.Content | safeHTML}}——它不校验内容,等同于信任任意字符串,XSS 风险极高
示例(Gin handler):
import "html/template"
func handler(c *gin.Context) {
content := "<p>Hello <strong>world</strong></p>"
c.HTML(http.StatusOK, "page.html", gin.H{
"Content": template.HTML(content), // 关键:强转为 template.HTML
})
}
为什么不能用 | safe 或直接用 string 传
常见错误是模板里写 {{.Content | safe}},或后端传 string 类型然后在模板里加 | safe。这等于告诉模板引擎:“我保证这串字符绝对安全”,但引擎不做任何检查——如果 .Content 实际来自用户输入且未过滤,XSS 就直接触发。
更隐蔽的问题是:即使你用了 | safe,它只影响当前 pipeline,无法阻止其他上下文(如 href="{{.URL | safe}}")中的 URL 编码或 JS 上下文转义,反而造成行为不一致。
真正可控的做法只有两个:
- 前端完全接管渲染(如用
innerHTML),但需确保后端返回的是已清洗的 HTML(如bluemonday处理后的结果) - 后端在传参前就用
template.HTML包装,让模板引擎从源头识别可信性
JSON 字符串传到前端也要用 template.JS
另一个高频场景:你想把 Go 结构体序列化后的 JSON 字符串塞进模板,供前端 JS 使用。如果直接传 string 类型,{{.Data}} 会把 { 转成 {,导致 JSON.parse() 报错 SyntaxError: Unexpected token &。
正确做法是用 template.JS 包装:
- 后端先
json.Marshal得到[]byte,转string后强转为template.JS - 传入模板的字段类型必须是
template.JS,不是string - 模板中仍写
{{.Data}},引擎识别后跳过 HTML 转义,输出原始 JSON 字符串
示例:
import (
"encoding/json"
"html/template"
)
data := struct{ Name string }{"Alice"}
b, _ := json.Marshal(data)
c.HTML(http.StatusOK, "page.html", gin.H{
"Data": template.JS(string(b)), // 注意:不是 string(b)
})
template.URL、template.CSS 等类型别名怎么选
Go 标准库提供了一组类型别名来声明内容语义,html/template 会据此选择对应的安全策略:
-
template.URL:用于href、src等 URL 属性,引擎会做url.QueryEscape,但跳过 HTML 转义 -
template.JS:用于内联 JS 或 JSON 字面量,跳过 HTML 转义,保留原始字节(不自动加引号) -
template.CSS:用于style属性,做 CSS 上下文编码 -
template.HTML:用于 HTML 内容,跳过 HTML 转义,但仍在其他上下文中做必要编码
关键点:这些类型只是“标注”,不是“开关”。它们不关闭所有编码,只切换编码策略。比如 template.URL("a(b)") 会输出 a%28b%29,而不是 a(b) —— 这是符合 RFC 的正确行为,不该被绕过。











