Go模板中html/template仅对{{.Field}}自动HTML转义,动态内容如{{template}}或|safeHTML会绕过防护;必须用bluemonday等库清理后转为template.HTML,禁用用户控制模板名或index路径,并配合CSP头防御XSS。

Go模板默认不自动转义HTML,必须显式处理
Go的html/template包在渲染时会自动对变量插值做HTML转义,但仅限于{{.FieldName}}这类直接引用;一旦用{{.HTMLContent | safeHTML}}或{{template "name" .}}引入外部内容,就绕过了默认防护。很多开发者误以为用了html/template就“安全了”,结果在动态拼接HTML、渲染用户提交的富文本时直接触发XSS。
- 只有
html/template提供自动转义,text/template完全不处理 —— 切勿在Web响应中混用 -
template函数调用默认继承父模板的上下文转义规则,但如果子模板里用了printf "%s"或index取值再输出,仍可能漏逃 - 自定义函数返回
template.HTML类型即等同于标记为“已清理”,后续不会再转义 —— 这是信任边界,别轻易返回
如何正确标记可信HTML内容
当确实需要渲染用户输入的有限HTML(如Markdown转义后的片段),必须用template.HTML显式封装,并确保上游已做过严格过滤。不能靠前端JS清理,也不能只靠正则替换就放行。
func renderPost(w http.ResponseWriter, r *http.Request) {
post := getPostFromDB(r.URL.Query().Get("id"))
// ✅ 正确:使用专用HTML清理库(如bluemonday)后,再转为template.HTML
cleaned := bluemonday.UGCPolicy().Sanitize(post.RawHTML)
data := struct {
Title string
Body template.HTML // 注意字段类型是template.HTML,不是string
}{
Title: post.Title,
Body: template.HTML(cleaned),
}
t.Execute(w, data)
}
- 永远不要用
strings.ReplaceAll或regexp.ReplaceAllString手动“删script标签”——绕过方式太多 -
template.JS、template.CSS、template.URL等类型对应不同上下文,不可混用;比如把URL传给template.JS会导致双引号被错误转义 - 若用
template.Must(template.New("t").Funcs(...))注册自定义函数,该函数返回值类型决定是否跳过转义 —— 返回template.HTML即跳过
避免在模板中执行危险操作
模板不是代码执行沙盒。Go模板支持{{if}} {{range}}等控制结构,但一旦允许用户控制{{template}}名称或{{with}}表达式路径,就可能触发任意数据读取甚至服务端模板注入(SSTI)。
- 禁止将用户输入直接拼进
{{template "user_" .UserID}}—— 模板名应来自白名单常量 - 禁止用
{{index .Data .UserKey}}访问map,除非.UserKey是服务端可控字段;否则可导致任意字段泄露(如.Data.Env.DB_URL) - 启用
template.Option("missingkey=error"),让未定义字段报错而非静默忽略 —— 减少因字段名拼错导致的意外空值穿透
HTTP头与CSP配合才是完整防线
模板安全只是XSS防御的一环。即使模板层100%正确,缺少Content-Security-Policy头仍可能被javascript:伪协议或内联事件绕过。
立即学习“go语言免费学习笔记(深入)”;
- 设置
Content-Type: text/html; charset=utf-8并带nosniff,防止MIME嗅探导致HTML被当作JS执行 - CSP至少包含
default-src 'self'; script-src 'self'; object-src 'none',禁用unsafe-inline和unsafe-eval - 对JSON API响应使用
application/json类型,并在模板中用json.Marshal预序列化后注入到data-属性,而非JSON.parse()解析内联脚本
最易被忽略的是:模板自动转义只管输出位置,不管数据来源。如果.UserInput本身是从数据库读出、且入库前没过滤,那转义只是“把变成zuojiankuohaophpcnscriptyoujiankuohaophpcn”,而攻击者早就在存入时用了绕过。源头清洗 + 模板转义 + CSP,缺一不可。










